Last updated

Setup Guide

How to integrate with TIN — onboarding, gRPC services, and validator discovery of your block-engine and P2C endpoints.

Audience: Block engines, searchers, and transaction landing services integrating with TIN.

Related: TIN overview · Post-pack · Tips · Revenue settlement

1. Joining TIN

To join TIN, share with the Rakurai team:

  1. Discovery endpoint — A global gRPC URL that returns the available regional block engine and P2C server endpoints.
  2. Settlement wallet pubkey — a wallet you control for PSA and MCA settlements.

Rakurai handles on-chain registration. Once registered, opted-in validators discover and connect to your endpoints automatically.

2. Sample application: tin_sample_servers

A minimal reference for TIN integration — gRPC services, auth, discovery, and basic packet handling.

Two example servers:

  • p2c_server — receive pre-confirmation (P2C) updates from Rakurai validators
  • bundles_server — send bundles to Rakurai validators

Use these as starting points; replace the dummy logic with your production block engine and P2C implementation. Build and run: Sample Server.

The connection is outbound from the validator.

The validator connects to the URL you provide. You run a gRPC server at that address. Bind to an address that validators can reach. Both http:// and https:// URLs are supported; https:// is recommended for production.

3. Protos

Your gRPC server must implement the services in these protobuf files. Do not rename services, RPCs, or fields — validators expect these exact names. Copy them from p2c-protos/protos (or from tin_sample_servers).

FileWhat it definesUsed for
auth.protoAuthService — challenge / token exchangeauthenticate before opening bundles or P2C stream
block_engine.protoBlockEngineValidator — discovery + bundle subscribe; BlockEngineRelayer — P2C streamsBundles path and P2C path
packet.protoPacket, PacketBatch, and related message typesWire shape of transactions on those streams
  • Bundles (server → validator): implement AuthService + BlockEngineValidator (GetBlockEngineEndpoints, SubscribePackets / SubscribeBundles). See bundles_server in tin_sample_servers.
  • P2C (validator → server): implement AuthService + BlockEngineRelayer (and GetBlockEngineEndpoints for discovery). See p2c_server in the same repo.

4. Contact Rakurai and share your wallet pubkey

On Discord or Telegram, share:

  1. Your global / discovery URL(s) — usually two: one for bundles (block engine) and one for P2C. If a single host implements both roles, share that one URL for both. Put all regional endpoints behind each discovery URL via GetBlockEngineEndpoints (global_endpoint + regioned_endpoints). Only the discovery URL is registered on-chain; regions are not shared with Rakurai one-by-one.
  2. A wallet pubkey you control (you hold the private key) — used for PSA / MCA recording and settlement

Rakurai registers the discovery URL(s) and creates per-validator PSA / MCA (and tip / TCA as needed). Keep that key secure for epoch settlement (rakurai-revshare). Details: §5.


5. Discovery — global URL for bundles and P2C

Both bundles and P2C use the same discovery RPC: GetBlockEngineEndpoints.

Share a discovery URL with Rakurai for each path you run:

  • Separate hosts — two URLs: one for bundles, one for P2C.
  • One host for both — one URL; that server must implement both the bundle (Validator) and P2C (Relayer) roles. See §9.

Validators open a gRPC connection to the discovery URL, request GetBlockEngineEndpoints, read the list you return, and connect to the closest / lowest-latency endpoint.

PathShare with RakuraiBehind it
BundlesDiscovery URLYour block-engine hosts
P2CDiscovery URLYour P2C / Relayer hosts

5.1. How it works

  1. You implement GetBlockEngineEndpoints on your server.
  2. You share that server’s public URL with Rakurai (this is the only URL registered on-chain).
  3. Validators send a GetBlockEngineEndpoints gRPC request and receive global_endpoint + regioned_endpoints.
  4. They pick the best URL from that list and open a gRPC connection to it.

If you run multiple servers (e.g. FRA, NYC, …):

  • Implement GetBlockEngineEndpoints on every server.
  • Return the same full list from each one — every regional URL you want validators to consider.
  • You do not register each region with Rakurai separately. Add or remove a region by updating the list your servers return.

P2C uses the same RPC. On a P2C-only host, implement GetBlockEngineEndpoints.

See bundles_server and p2c_server.

5.2. Request / response

Request:

GetBlockEngineEndpointRequest {}

Example response (one global + two regions):

GetBlockEngineEndpointResponse {
  global_endpoint {
    block_engine_url: "https://gateway.your-provider.com"
    shredstream_receiver_address: ""
  }
  regioned_endpoints {
    block_engine_url: "https://fra.gateway.your-provider.com"
    shredstream_receiver_address: ""
  }
  regioned_endpoints {
    block_engine_url: "https://nyc.gateway.your-provider.com"
    shredstream_receiver_address: ""
  }
}
FieldRequired?Meaning
global_endpointYesPrimary URL for your service.
regioned_endpointsYesFull list of regional URLs. If you have only one region, return the same URL as global_endpoint. Additional regional endpoints can be added as your infrastructure expands.
block_engine_urlYesPublic URL validators can reach.
shredstream_receiver_addressNoLeave as ""
One region is enough to start, but not recommended for production.

Since validators are distributed across different geographic locations, you may need multiple regional servers to achieve the best latency. As your service expands, you can add regional endpoints in different locations, allowing validators to connect to the endpoint with the lowest latency.

Return public URLs only

Every block_engine_url must be reachable from remote validators. Loopback or bind-all addresses (localhost, 127.0.0.1, 0.0.0.0) will not work.

5.3. How to check your server

Before you share a URL with Rakurai, confirm both of these respond on that host. If either fails, validators cannot connect.

CheckRPCPasses when
DiscoveryBlockEngineValidator/GetBlockEngineEndpointsYou get back global_endpoint / regioned_endpoints with public URLs
AuthAuthService/GenerateAuthChallengeYou get back a challenge string

Install grpcurl if needed, then from the proto directory:

1. Discovery

cd path_to_tin_sample_servers/tin_sample_servers/protos/

grpcurl -plaintext -import-path . -proto block_engine.proto \
  <HOST>:<PORT> \
  block_engine.BlockEngineValidator/GetBlockEngineEndpoints

2. Auth (use VALIDATOR for bundles, RELAYER for P2C)

cd p2c-protos/protos
grpcurl -plaintext -import-path . -proto auth.proto \
  -d '{"role":"VALIDATOR","pubkey":"Ed9WjPnZfAXsPttcqxMwj94qsuXVRyBsyXnDkxFva2Zv"}' \
  <HOST>:<PORT> \
  auth.AuthService/GenerateAuthChallenge

Run both checks against every regional host you list in regioned_endpoints, not only the discovery URL.


6. Auth (Used by both Block Engine and P2C)

Both bundles and P2C use auth.AuthService. Role differs by path.

PathRole
Bundles (BlockEngineValidator streams)VALIDATOR
P2C (BlockEngineRelayer streams)RELAYER

Flow:

  1. GenerateAuthChallenge — { role, pubkey } → { challenge }
  2. Client signs "{pubkey}-{challenge}", then GenerateAuthTokens → { access_token, refresh_token }
  3. Later RPCs: authorization: Bearer <access_token.value>
GenerateAuthChallengeRequest { role: VALIDATOR, pubkey: <32 bytes> }
GenerateAuthChallengeResponse { challenge: "…" }

GenerateAuthTokensRequest {
  challenge: "<pubkey>-<challenge>"
  client_pubkey: <32 bytes>
  signed_challenge: <64-byte sig>
}
GenerateAuthTokensResponse {
  access_token { value: "…" expires_at_utc { … } }
  refresh_token { value: "…" expires_at_utc { … } }
}

Reject identities that are not allowed to connect (e.g. not on the leader schedule / not a Rakurai client).


7. Setup Bundle Server BlockEngineValidator

After VALIDATOR authentication, the validator opens subscription streams. Your block engine keeps these streams open and pushes packets and bundles to the validator for the lifetime of the connection.

RPCRequired?DirectionPurpose
GetBlockEngineEndpointsYes (discovery host)Validator → youReturns available block-engine endpoints
SubscribePacketsYes for bundlesYou → validatorStreams transaction packets
SubscribeBundlesYes for bundlesYou → validatorStreams bundles for inclusion

7.1. Request / Response Schemas

# Requests (empty) + Bearer authentication
SubscribePacketsRequest {}
SubscribeBundlesRequest {}

# Stream responses
SubscribePacketsResponse {
  header { ts { … } }
  batch {
    packets {
      data: <wire tx>
      meta { size: … }
    }
  }
}

SubscribeBundlesResponse {
  bundles {
    uuid: "…"
    bundle {
      packets {
        data: <wire tx>
        …
      }
    }
  }
}

Keep both streams open for the lifetime of the connection.

Once connected, send transactions or bundles with a tip to a Rakurai tip account. The scheduler uses the tip to prioritize them for inclusion.

Minimum Tip Requirements
  1. P2C MEV: 0.001 SOL — Minimum tip to receive the 20% virtual priority boost on P2C MEV bundles. A lower tip may result in reduced QoS and no priority boost.
  2. Non-P2C: 0.0005 SOL — Recommended tip for regular non-P2C bundles to get the best QoS. Lower tip may lead to potentially lower QoS.
  3. Below 100K lamports: Bundles with low tips are processed on a best-effort basis.

8. Setup P2C Server BlockEngineRelayer

After RELAYER authentication, the validator opens one TLS/channel and up to three bidirectional streams. Your relayer keeps these streams open and sends the relevant transaction updates for the lifetime of the connection.

RPCRequired?DirectionPurpose
StartExpiringPacketStreamYes for resellValidator → serverPost-pack confirmations for reselling
StartExpiringMevPacketStreamYes for mevValidator → serverPost-pack confirmations for backrunning
StartExpiringTpuPacketStreamOptionalValidator → serverStreams non-leader TPU transactions for backrunning
StartP2cUpdateCountStreamOptionalValidator → serverStreams per-slot transaction counts for usage and settlement

All packet streams use the same PacketBatchUpdate message. The gRPC method identifies the source of the packet; there is no separate source field in the message. Your server may return UNIMPLEMENTED for TPU and PacketCount streams.

8.1. Differentiating Scheduler vs TPU

The streams are different points in the pipeline. Which post-pack confirmation RPC you get depends on the endpoint’s p2c_type — and on how you use P2C.

StreamWhenWho gets itMeaning
StartExpiringPacketStreamValidator is the leaderPost-pack (no boost)
StartExpiringMevPacketStreamValidator is the leaderPost-pack (boost-eligible)
StartExpiringTpuPacketStreamValidator is not the leaderTPU path — validator forwards a received packet to you. This does not guarantee inclusion ahead of other transactions.
Identify the stream by the gRPC method

All streams send the same PacketBatchUpdate shape. There is no source field on the packet. If it arrived on StartExpiringPacketStream, it is Resell scheduler / post-pack; if on StartExpiringMevPacketStream, it is Mev scheduler / post-pack; if on StartExpiringTpuPacketStream, it is TPU.

meta differences

Treat meta.addr as opaque proof / correlation data — do not parse it as a socket address. Full layout: Using P2C — meta fields.

FieldScheduler streamTPU stream
meta.sizedata.len()data.len()
meta.addrPer-tx uuid (not an IP addr)Validator identity signature over the transaction-signature

8.2. P2C Slot Tracking (expiry_ms)

Each P2C update (post-pack confirmation and TPU) includes an expiry_ms field:

expiry_ms: <slot as u32>

Despite its name, expiry_ms is not a millisecond timeout. On the P2C path, it contains the validator’s slot (u32) at the time the update was sent.

Use this value to group updates, debounce duplicates, and time reply bundles against the leader slot that produced the update. When the slot changes, treat it as a new window.

8.3. P2C Update Counts

When StartP2cUpdateCountStream is enabled, the validator sends one P2cUpdateCount per slot and flushes the current slot on disconnect.

FieldMeaning
uuidPost-pack endpoint UUID
slotSlot the counts belong to
scheduler_countTransactions successfully sent on the scheduler/post-pack streams for that slot
tpu_countTransactions successfully sent on the TPU stream for that slot
total_countscheduler_count + tpu_count
p2c_tpu_enabledWhether TPU updates are enabled for this endpoint

Use these counts as a health and volume signal instead of counting packets yourself. They show whether the leader is connected, the amount of scheduler vs. TPU traffic received, and can help detect silent drops.

8.4. Packet / count schemas

# Validator → you (scheduler or TPU stream)
PacketBatchUpdate {
  batches {
    batch { packets { data: <wire tx> meta { size: … addr: "<p2c string>" port: 0 } } }
    # Field name is historical; value is the working-bank slot (u32), not a timeout in ms
    expiry_ms: <slot_u32>
  }
}

# Validator → you (count stream)
P2cUpdateCount {
  uuid: "…"
  slot: …
  scheduler_count: …
  tpu_count: …
  total_count: …
  p2c_tpu_enabled: true|false
}

# You → validator (heartbeats on each open Relayer stream)
StartExpiringPacketStreamResponse { heartbeat { count: 1 } }

Packet decoding and reply bundles: Using P2C.


9. Same URL for bundles and P2C

You can run bundles and P2C on separate hosts (two discovery URLs) or on one host (one discovery URL). If you use one URL, that server must expose both roles:

ServiceWhat the validator uses
auth.AuthServiceVALIDATOR and RELAYER
block_engine.BlockEngineValidatorGetBlockEngineEndpoints, SubscribePackets / SubscribeBundles
block_engine.BlockEngineRelayerStartExpiringPacketStream if resell; StartExpiringMevPacketStream if mev; TPU if Mev+flag; count optional
Same URL without the Relayer role

Bundles can work while P2C fails`.

Separate URLs are fine (Validator-only vs Relayer-only hosts). Relayer-only hosts still need GetBlockEngineEndpoints for P2C region autoconfig.


10. Sample Server

tin_sample_servers provides a working reference implementation:

  • bundles_server — Auth, BlockEngineValidator and GetBlockEngineEndpoints.
  • p2c_server — Auth, BlockEngineRelayer, GetBlockEngineEndpoints, plus TPU and update-count streams enabled by default.
cargo build -p bundles_server --release

RUST_LOG=info ./target/release/bundles_server \
  --bind 0.0.0.0:10000 \
  --public-url http://<PUBLIC_HOST>:10000 \
  --allow-any-validator
cargo build -p p2c_server --release

RUST_LOG=info ./target/release/p2c_server \
  --bind 0.0.0.0:10001 \
  --public-url http://<PUBLIC_HOST>:10001

# Optional:
# --disable-tpu-packet-stream
# --disable-p2c-update-count

--bind is the local listen address; 0.0.0.0 is fine. --public-url is the URL returned by GetBlockEngineEndpoints and must be reachable by validators. Do not use 127.0.0.1 or 0.0.0.0.

Share the discovery URL with the Rakurai team. Authentication is limited to pubkeys on getLeaderSchedule that advertise the Rakurai client ID in getClusterNodes.


11. Validator — Check Block Engine and P2C Endpoints

Validators can use Admin RPC commands on the validator to inspect the configured and active Block Engine (bundle) and P2C (post-pack) endpoints, and optionally disconnect/blocklist an endpoint by its UUID.

Workflow:

  1. Call the get method for each service to retrieve the configured and active endpoints.
  2. Identify the uuid of the endpoint you want to manage.
  3. Use the corresponding Admin RPC blocklist command to disconnect and blocklist that endpoint.

Run these commands from the validator host, with admin.rpc located in the /mnt/ledger directory.

ServiceInspectDisconnect
Block Engine (Bundles)getBlockEngineUrlssetBlockEngineUrlBlocklist
P2C (Post-Pack)getPostPackConfirmationConfigsetPostPackConfirmationUuidBlocklist

11.1. Block Engine — getBlockEngineUrls

Shows the primary Block Engine URL plus secondary entries (set on-chain).

(echo '{"jsonrpc":"2.0","id":1,"method":"getBlockEngineUrls","params":[]}'; sleep 1) \
  | socat - UNIX-CONNECT:admin.rpc | jq

Example response:

{
    "primary_url": "https://frankfurt.mainnet.block-engine.jito.wtf",
    "admin_secondary_entries": [],
    "onchain_secondary_entries": [],
    "active_secondary_entries": [],
    "blocklisted_uuids": []
}
FieldDescription
primary_urlCLI-configured Block Engine URL (Jito)
onchain_secondary_entriesSecondary BE entries loaded from the on-chain config PDA
active_secondary_entriesMerged on-chain secondary BE (excluding blocklisted UUIDs)
blocklisted_uuidsSecondary BE UUIDs currently blocklisted

11.2. P2C — getPostPackConfirmationConfig

Shows post-pack (P2C) endpoints: on-chain and which UUIDs are active.

(echo '{"jsonrpc":"2.0","id":1,"method":"getPostPackConfirmationConfig","params":[]}'; sleep 1) \
  | socat - UNIX-CONNECT:admin.rpc | jq
FieldDescription
onchain_entriesEntries from the on-chain PDA (url, uuid, enable_tpu_p2c_update)
blocklisted_uuidsUUIDs blocked via Admin RPC
active_entriesMerged on-chain (excluding blocklist) — endpoints that receive Relayer streams

11.3. Blocklists

Each set* call replaces the full blocklist for that service. Pass one UUID array to block; pass an empty inner array to clear blocklist.

Block Engine (blocks secondary BE entries by UUID; does not clear primary_url):

# Block one UUID
(echo '{"jsonrpc":"2.0","id":1,"method":"setBlockEngineUrlBlocklist","params":[["<BlockEngine1>"]]}'; sleep 1) \
  | socat - UNIX-CONNECT:admin.rpc

# Clear — must be params:[[]] not params:[]
(echo '{"jsonrpc":"2.0","id":1,"method":"setBlockEngineUrlBlocklist","params":[[]]}'; sleep 1) \
  | socat - UNIX-CONNECT:admin.rpc

P2C (removes matching endpoints from active_entries on the next config sync; open connections to blocklisted UUIDs are torn down):

(echo '{"jsonrpc":"2.0","id":1,"method":"setPostPackConfirmationUuidBlocklist","params":[["PostPackConfig1"]]}'; sleep 1) \
  | socat - UNIX-CONNECT:admin.rpc

(echo '{"jsonrpc":"2.0","id":1,"method":"setPostPackConfirmationUuidBlocklist","params":[[]]}'; sleep 1) \
  | socat - UNIX-CONNECT:admin.rpc
Clearing the blocklist

Use params:[[]]. Writing params:[] omits the parameter and the blocklist is not cleared.