Shards and routing
MultiversX splits its state across shards. Which shard an account belongs to is determined by the last byte of its address — it is a property of the address, fixed forever, and anyone can compute it without asking a node.
This matters here for one reason: the fee payer of a relayed transaction has to be in the same shard as the sender. So "the CoRelayer relayer" is not one address. It is a set, with coverage in every shard, and part of the service is telling you which one is yours.
The assignment
Before you sign anything, you ask:
POST /v1/relay/assign
{ "sender": "erd1…", "proof": { "kind": "key", "serverTimeMs": 1789000000000, "signature": "…" } }
The request is the same whoever pays: the sender's key signs the proof. When your server pays for the sender with a sponsor key, the key travels on the relay call that follows, not here:
POST /v1/relay
X-Api-Key: crk_live_…
{ "tx": { "sender": "erd1…", "relayer": "erd1…", "signature": "…" }, "lease": "…" }
From the assignment you get back everything the transaction needs:
| Field | What you do with it |
|---|---|
relayer | Put it in the relayer field before signing. |
shard | The shard both of you are in. Informational. |
lease | An opaque token you send back with the signed transaction. |
leaseExpiresAtMs | Sixty seconds after issue. |
serverTimeMs | Correct your clock against this rather than trusting the local one. |
chainId | Compare it with the chainID you are about to sign. They must match. |
minGasPrice, maxGasPrice | The band your gas price must fall in. |
extraGasRelayed, extraGasGuarded | The protocol surcharges to add to the gas limit. |
expectedNonce | The nonce the service believes is next for this sender. |
pinNonce | Present when the flow must use a specific nonce — a cancel, or a re-sign. |
registryVersion | Changes when the relayer set changes; use it as a cache key. |
Why the assignment needs a proof
Assignment reserves nothing and costs nothing, so it is tempting to make it public. It is not, and the reason is specific: a lease issued to anyone who merely knows an address would let a third party who is holding an old, signed-but-never-relayed payload bring it back to life. The proof is a signature by the sender's own key over a short message:
corelayer/assign/v1|<chainId>|<sender>|<serverTimeMs>
valid within thirty seconds of serverTimeMs. The chain id is inside the message, so a proof
captured on one network cannot mint a lease on another.
If you already hold a native-auth token for the sender, made for one of CoRelayer's own origins, that stands in for the proof. If you are a service paying for other people's transactions, the proof is the same, signed with the sender's key, and your sponsor key goes on the relay call. (Authentication, Paying for other senders)
What the lease is, and is not
A lease is a short-lived, authenticated token minted by the signer. It says: this relayer, for this sender, until this moment.
| It is | It is not |
|---|---|
| A freshness proof — evidence the client is here now | A reservation. Nothing is held for you |
| A routing decision — which relayer will co-sign | A promise. Entitlement is checked at relay time |
| Verified again by the signer before co-signing | Something a client can construct |
Because it reserves nothing, asking twice is harmless: you get two valid leases for the same relayer. Because it is verified again inside the signer, a leaked lease alone gets nobody anything — it still needs a validly signed transaction naming that relayer.
How the relayer is chosen
Within your shard, the choice is deterministic: a rendezvous hash over the shard's currently active relayers. Two properties follow, and both are useful:
- It is stable. The same sender gets the same relayer as long as the active set does not change, which keeps nonce handling and exposure accounting simple on our side.
- It is not a secret. The compatibility endpoint
GET /relayer/address/{userAddress}returns the same answer without a lease, for clients that cache one relayer address forever.
When the active set changes — a relayer is drained, a spare is activated — registryVersion moves
and the mapping re-settles. That is also the moment an old cached relayer address may stop working.
Verifying the relayer yourself
You do not have to take our word for which address is a real CoRelayer relayer. The registry lives in the contract:
getRelayerState(address)returns the registry state; only an active relayer should ever be co-signing for you;getRegistryVersion()tells you whether your cached answer is still current;getActiveRelayers()lists them.
Do this before signing, through a gateway that is not ours, and cache the result by
registryVersion. The contract address you query is pinned in your own configuration — never
taken from an API response, because an API that could name its own contract could name any
contract. Each CoRelayer SDK runs this check for you when you give its relay flow a relayer
verifier. Verify a relayer shows how to turn it on, and lists the steps
for a program that calls the API directly.
Relayer states
| State | Meaning for you |
|---|---|
registered | Known to the contract, not serving. Never assigned. |
active | Serving. This is the only state an assignment ever names. |
draining | Still co-signs transactions that were already promised, takes no new ones. |
retired | Finished. A relayer is never retired while a transaction it co-signed is still in flight. |
A relayer that is handed out by the compatibility endpoint — which clients cache without any expiry — drains for a full month before it can retire, because there is no way to tell those clients to look again.
Next
- What the relayer set is for and how it is funded: Relayers
- What happens once a transaction is co-signed: Delivery guarantees
- The rest of the assignment fields, precisely: API overview