Delivery guarantees
The unit of work is an intent: one user-signed transaction we were asked to relay. Its key is
(sender, nonce) — never the transaction hash, because the hash is an attribute of one attempt and
the slot is the thing that can only be used once.
The states
| State | What it tells you |
|---|---|
ASSIGNED | You hold a lease. Nothing exists on our side. |
RECEIVED | We have your bytes and are checking them. |
REJECTED | Terminal. No signature was produced and nothing was sent. |
ACCEPTED | Checks passed, Relay Units reserved, about to be co-signed. |
COSIGNED | Past the commit point. The transaction exists. |
BROADCAST | A gateway took it. |
EXECUTED_OK | On chain, succeeded. |
EXECUTED_FAIL | On chain, reverted. Charged — see below. |
EXPIRED_LOCALLY_STILL_VALID | We stopped re-broadcasting. The transaction is still valid and may yet execute. Non-terminal on purpose. |
DEAD | The nonce was consumed by a different transaction. Ours can never execute; the reservation is released. |
The commit point
The commit point is the moment a relayer signature leaves the signer process. It divides the whole system:
Before it nothing exists that can execute. Every validation failure, every entitlement problem, every rate limit, every "no relayer available" lives here. A client that receives an error has a guarantee: nothing was sent.
After it the transaction exists in the world and, by the protocol, has no expiry. So after the
commit point the API stops returning errors and returns states: 200 when a gateway has taken
it, 202 when it is co-signed but not yet acknowledged. "Failed" is not an available answer,
because it would not be true.
This is the property to design your client around. An error from POST /v1/relay always means the
transaction was not sent.
Never sent twice
Three mechanisms, in layers, each closing what the previous one cannot see.
The protocol
At most one transaction per (sender, nonce) executes, and identical bytes re-broadcast any number
of times are the same transaction. Re-broadcasting is therefore not a risk; it is the correct
response to uncertainty.
The natural key
POST /v1/relay is idempotent on (sender, nonce) plus a hash of the signed bytes:
| You send | You get |
|---|---|
| Identical bytes again | The stored response, marked duplicate: true. No second reservation, no second broadcast decision. |
| Different bytes, same nonce, first still alive | NONCE_IN_FLIGHT — unless you explicitly asked to cancel or replace. |
This works across hosts because Ed25519 signing is deterministic and the ledger has a uniqueness constraint on the slot.
The logical key
The natural key cannot see one specific mistake: a client that times out, assumes failure, and
signs a fresh transaction at nonce n+1 for the same business action while T(n) is still
alive. Different nonce, different bytes — both valid, both will execute.
So there is an optional logical key, one per user action: the Idempotency-Key header, or
intentKey in the body.
POST /v1/relay
Idempotency-Key: checkout-7f2c0a41
Same key, different bytes or nonce, while the first intent is not DEAD →
INTENT_ALREADY_SUBMITTED, carrying the first intent so you
can look at it instead of guessing. Once the first intent is DEAD, the key is free again.
The key can only ever reject; it can never disagree with the natural key. The SDK, the MCP server and the dashboard set one automatically, one per user action.
When something fails
Three cases, three different answers. None of them asks the user to sign the same thing twice.
| What fails | What happens |
|---|---|
| A gateway is unreachable | The identical signed bytes go to another gateway. Nothing is rebuilt, so there is nothing new to sign and nothing new that could execute. |
| The relayer runs low on EGLD | A balance watcher keeps every assignable relayer above a floor. A relayer below it stops being assignable before it stops being able to pay, and topping it up lets the transaction already signed go through. |
| The relayer is lost before it co-signs | Only then does the API answer that a new signature is needed: one signature over the same account nonce, so the old transaction and the new one can never both execute. (Handle a re-sign) |
What to do on a timeout
On a timeout or a dropped connection from POST /v1/relay:
→ re-send the identical bytes, or
→ GET /v1/intents/{sender}/{nonce}
Never rebuild. Never re-sign.
GET /v1/intents/{sender}/{nonce} is the authoritative read. It is answered correctly by any host,
including one that did not handle your submission.
What gets charged
| Outcome | Relay Units |
|---|---|
| Executed, succeeded | Charged, at the weight of the executed transaction. |
| Executed, reverted | Charged. The network charges the relayer the full gas limit for a failed execution and refunds nothing. |
| Rejected before co-signing | Nothing. There was no cost. |
| Not executable at all — nonce already used, fee refused | Nothing. The network charges nothing for these. |
DEAD — the slot was taken by another transaction | Nothing. The reservation is released. |
| Co-signed, still waiting | Reserved, not yet charged. |
Units are reserved at the worst case before co-signing, and settled at the real weight after execution. That is why a transaction with a needlessly high gas limit costs you more than it should — the reservation has to assume the network will take all of it. (Relay Units)
The state that surprises people
EXPIRED_LOCALLY_STILL_VALID means: we have stopped actively re-broadcasting, but your transaction
is a perfectly valid transaction that any node may still include. It is not a failure and it is
not terminal. The honest answer at that point is "unknown", and the wrong answer would be to
tell you it failed and let you sign a replacement — which is precisely how two transactions for one
intention get created.
From that state, the safe moves are: wait, re-send the identical bytes, or explicitly cancel or replace the slot with a pinned nonce.
Watching an intent
| Way | Good for |
|---|---|
GET /v1/relay/{id}/events | Server-sent events, one per state change. Closes when the intent is final. |
GET /v1/intents/{sender}/{nonce} | A definitive answer at any moment, from any host. |
GET /v1/relay/{id} | A resolver that takes either an intent id or a transaction hash. |
| The account stream | Everything for one account on one connection. |
POST /v1/relay itself never waits. It answers at the commit point or at its acknowledgement
deadline, because a held-open request would turn every slow block into an ambiguous client timeout
— which is the exact condition that makes clients re-sign.
(Watch an intent)