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:
- Discovery endpoint — A global gRPC URL that returns the available regional block engine and P2C server endpoints.
- 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 validatorsbundles_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 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).
| File | What it defines | Used for |
|---|---|---|
auth.proto | AuthService — challenge / token exchange | authenticate before opening bundles or P2C stream |
block_engine.proto | BlockEngineValidator — discovery + bundle subscribe; BlockEngineRelayer — P2C streams | Bundles path and P2C path |
packet.proto | Packet, PacketBatch, and related message types | Wire shape of transactions on those streams |
- Bundles (server → validator): implement
AuthService+BlockEngineValidator(GetBlockEngineEndpoints,SubscribePackets/SubscribeBundles). Seebundles_serverintin_sample_servers. - P2C (validator → server): implement
AuthService+BlockEngineRelayer(andGetBlockEngineEndpointsfor discovery). Seep2c_serverin the same repo.
4. Contact Rakurai and share your wallet pubkey
On Discord or Telegram, share:
- 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. - 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.
| Path | Share with Rakurai | Behind it |
|---|---|---|
| Bundles | Discovery URL | Your block-engine hosts |
| P2C | Discovery URL | Your P2C / Relayer hosts |
5.1. How it works
- You implement
GetBlockEngineEndpointson your server. - You share that server’s public URL with Rakurai (this is the only URL registered on-chain).
- Validators send a
GetBlockEngineEndpointsgRPC request and receiveglobal_endpoint+regioned_endpoints. - 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
GetBlockEngineEndpointson 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: "" } }
| Field | Required? | Meaning |
|---|---|---|
global_endpoint | Yes | Primary URL for your service. |
regioned_endpoints | Yes | Full 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_url | Yes | Public URL validators can reach. |
shredstream_receiver_address | No | Leave as "" |
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.
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.
| Check | RPC | Passes when |
|---|---|---|
| Discovery | BlockEngineValidator/GetBlockEngineEndpoints | You get back global_endpoint / regioned_endpoints with public URLs |
| Auth | AuthService/GenerateAuthChallenge | You 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.
| Path | Role |
|---|---|
Bundles (BlockEngineValidator streams) | VALIDATOR |
P2C (BlockEngineRelayer streams) | RELAYER |
Flow:
GenerateAuthChallenge—{ role, pubkey }→{ challenge }- Client signs
"{pubkey}-{challenge}", thenGenerateAuthTokens→{ access_token, refresh_token } - 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.
| RPC | Required? | Direction | Purpose |
|---|---|---|---|
GetBlockEngineEndpoints | Yes (discovery host) | Validator → you | Returns available block-engine endpoints |
SubscribePackets | Yes for bundles | You → validator | Streams transaction packets |
SubscribeBundles | Yes for bundles | You → validator | Streams 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.
- 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.
- 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.
- 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.
| RPC | Required? | Direction | Purpose |
|---|---|---|---|
StartExpiringPacketStream | Yes for resell | Validator → server | Post-pack confirmations for reselling |
StartExpiringMevPacketStream | Yes for mev | Validator → server | Post-pack confirmations for backrunning |
StartExpiringTpuPacketStream | Optional | Validator → server | Streams non-leader TPU transactions for backrunning |
StartP2cUpdateCountStream | Optional | Validator → server | Streams 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.
| Stream | When | Who gets it | Meaning |
|---|---|---|---|
StartExpiringPacketStream | Validator is the leader | Post-pack (no boost) | |
StartExpiringMevPacketStream | Validator is the leader | Post-pack (boost-eligible) | |
StartExpiringTpuPacketStream | Validator is not the leader | TPU path — validator forwards a received packet to you. This does not guarantee inclusion ahead of other transactions. |
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.
| Field | Scheduler stream | TPU stream |
|---|---|---|
meta.size | data.len() | data.len() |
meta.addr | Per-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_msis 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.
| Field | Meaning |
|---|---|
uuid | Post-pack endpoint UUID |
slot | Slot the counts belong to |
scheduler_count | Transactions successfully sent on the scheduler/post-pack streams for that slot |
tpu_count | Transactions successfully sent on the TPU stream for that slot |
total_count | scheduler_count + tpu_count |
p2c_tpu_enabled | Whether 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:
| Service | What the validator uses |
|---|---|
auth.AuthService | VALIDATOR and RELAYER |
block_engine.BlockEngineValidator | GetBlockEngineEndpoints, SubscribePackets / SubscribeBundles |
block_engine.BlockEngineRelayer | StartExpiringPacketStream if resell; StartExpiringMevPacketStream if mev; TPU if Mev+flag; count optional |
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,BlockEngineValidatorandGetBlockEngineEndpoints.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:
- Call the get method for each service to retrieve the configured and active endpoints.
- Identify the
uuidof the endpoint you want to manage. - 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.
| Service | Inspect | Disconnect |
|---|---|---|
| Block Engine (Bundles) | getBlockEngineUrls | setBlockEngineUrlBlocklist |
| P2C (Post-Pack) | getPostPackConfirmationConfig | setPostPackConfirmationUuidBlocklist |
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": [] }
| Field | Description |
|---|---|
primary_url | CLI-configured Block Engine URL (Jito) |
onchain_secondary_entries | Secondary BE entries loaded from the on-chain config PDA |
active_secondary_entries | Merged on-chain secondary BE (excluding blocklisted UUIDs) |
blocklisted_uuids | Secondary 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
| Field | Description |
|---|---|
onchain_entries | Entries from the on-chain PDA (url, uuid, enable_tpu_p2c_update) |
blocklisted_uuids | UUIDs blocked via Admin RPC |
active_entries | Merged 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
Use params:[[]]. Writing params:[] omits the parameter and the blocklist is not cleared.
Related
- tin_sample_servers — sample Auth + Validator / Relayer servers
- Tips — tip accounts, virtual priority, TCA
- Post-pack confirmations — PSA / MCA
- Using P2C — packet layout, reply bundles