Relay Protocol
WebSocket wire protocol specification for communication between the AXIO relay and client validators.
Relay Protocol
This document defines the wire protocol between the AXIO relay and client validators.
Overview
AXIO Backend Client Validator
┌────────────┐ ┌──────────────┐
│ axio-relay │ ──── WebSocket ────▶ │ forti-bundle │
│ │ ◀─── Confirmations ── │ (crate) │
└────────────┘ └──────────────┘Transport
| Property | Value |
|---|---|
| Protocol | WebSocket (ws:// or wss://) |
| Default Port | 4002 |
| Path | /v1/relay |
| Auth | X-Auth-Token header on WebSocket upgrade |
| Encoding | JSON |
Connection Flow
- Client connects:
wss://relay.axio.fortiblox.com/v1/relay - Client sends
Registermessage with validator identity - Server confirms registration with
Registeredresponse - Server pushes bundles when client has upcoming leader slots
- Client confirms inclusion or rejection
Message Types
Client → Server
Register
Sent immediately after WebSocket connection is established.
{
"type": "register",
"validator": "<base58 identity pubkey>",
"version": "1.0.0",
"capabilities": ["bundle-v1"],
"mev_commission_bps": 500
}| Field | Type | Description |
|---|---|---|
validator | string | Base58 validator identity pubkey |
version | string | Client software version |
capabilities | string[] | Supported protocols |
mev_commission_bps | number | MEV commission in basis points |
BundleConfirmation
Sent after a bundle's target slot has passed, reporting the inclusion result.
{
"type": "bundle_confirmation",
"bundle_id": "<uuid>",
"status": "included",
"slot": 149900000,
"signatures": ["<base58 sig>", "..."],
"reason": null
}| Field | Type | Description |
|---|---|---|
bundle_id | string | UUID of the delivered bundle |
status | string | "included" | "rejected" | "expired" |
slot | number | Slot where the bundle was (or should have been) included |
signatures | string[] | On-chain transaction signatures (empty if rejected/expired) |
reason | string? | Optional rejection reason |
Heartbeat
Sent every 10 seconds to maintain the connection.
{
"type": "heartbeat",
"slot": 149900000,
"timestamp": 1712750000000
}Server → Client
Registered
Confirmation that the validator is registered and will receive bundles.
{
"type": "registered",
"status": "ok",
"relay_version": "1.0.0"
}BundleDelivery
A winning bundle from the auction, delivered just-in-time before the validator's leader slot.
{
"type": "bundle_delivery",
"bundle_id": "<uuid>",
"target_slot": 149900001,
"transactions": [
{
"data": "<base64 serialized tx>",
"accounts": [
{ "pubkey": "<base58>", "isWritable": true }
]
}
],
"tip": 5000000,
"ev_score": 7500000.0,
"priority": 1,
"expires_at_slot": 149900013
}| Field | Type | Description |
|---|---|---|
bundle_id | string | UUID for tracking |
target_slot | number | Slot to include this bundle |
transactions | array | Ordered list of transactions |
tip | number | Tip amount in lamports |
ev_score | number | Expected value score from auction |
priority | number | Priority rank (1 = highest) |
expires_at_slot | number | Discard after this slot |
SlotAssignment
Notifies the validator of its upcoming leader slots (sent at the start of each epoch and when the schedule updates).
{
"type": "slot_assignment",
"slots": [149900100, 149900101, 149900102, 149900103],
"epoch": 299800
}Bundle Inclusion Contract
When a client validator receives a BundleDelivery:
- MUST include transactions in the specified order (atomic)
- MUST include the tip transaction (last tx in bundle)
- MUST respect
expires_at_slot— discard if past - SHOULD include higher-priority bundles first
- MUST send
BundleConfirmationafter the slot
For multi-tx bundles (2-5 transactions), the validator must execute them sequentially with state carryover — each transaction sees the effects of the previous one.
Tip Distribution
The tip transaction is always the last transaction in the bundle. It transfers lamports from the searcher to one of 8 rotating tip PDA vaults managed by the forti-tips program.
Program: 7dWho2FmC68LZBirmAf68GsrEJSFjpaVTF5pYPqVDCug
Fee Split:
40% → Validator
25% → Reserve injection (aXNT backing)
15% → Staker yield
20% → TreasuryA permissionless crank triggers tip distribution every 25-40 seconds (randomized to prevent frontrunning).
Error Handling
| Scenario | Behavior |
|---|---|
| WebSocket disconnects | Client reconnects with exponential backoff (1s, 2s, 4s, 8s, max 30s) |
| Stale heartbeat (>10s) | Server stops sending bundles until heartbeat resumes |
| Invalid auth token | Server closes connection with code 4001 |
| Bundle expired at delivery | Client discards silently, sends expired confirmation |
| All validators disconnected | Relay queues bundles for up to 30s, then drops |
Multi-Validator Broadcast
AXIO supports multi-validator broadcast. When RELAY_BROADCAST_COUNT > 1, the relay delivers each winning bundle to N validators simultaneously. The first validator to include the bundle wins the tip. This provides redundancy and ensures bundles are included even if one validator misses a slot.
Versioning
| Property | Value |
|---|---|
| Protocol version | In URL path: /v1/relay |
| Breaking changes | New path: /v2/relay |
| Client capabilities | Declared in Register message |
| Backward compat | v1 supported for 6+ months after v2 launch |