Relay Units
A Relay Unit (RU) is the unit of everything CoRelayer sells. A plan is a number of Relay Units per month. Pay-as-you-go is a price per Relay Unit. The single price lever on chain, the tariff, is expressed per Relay Unit.
One Relay Unit is 0.001 EGLD of worst-case network fee. It is defined in terms of the fee because the fee is the real cost of relaying; a unit defined any other way would drift away from what the service actually spends.
The formula
Schedule version 1:
RU_SIZE_ATTO = 1_000_000_000_000_000 // 0.001 EGLD
moveGas = erd_min_gas_limit
+ erd_gas_per_data_byte × len(data) // data = raw bytes of the data field
+ erd_extra_gas_limit_relayed_tx // always: everything we relay is relayed v3
+ (guarded ? erd_extra_gas_limit_guarded_tx : 0)
procPrice = ceil(gasPrice / 100) // integer form: (gasPrice + 99) / 100
maxFee = moveGas × gasPrice + (gasLimit − moveGas) × procPrice
ru = max(1, ceil(maxFee / RU_SIZE_ATTO))
Three things are worth reading twice:
- It is integer arithmetic on the fields of the signed transaction. No floats, no oracle, no server-side discretion. You can compute the cost of your own transaction before you sign it and get the same answer the service does.
- It prices the worst case, not the gas used. A relayed transaction that reverts still costs the relayer its full gas limit, and the network refunds nothing above a small ceiling. Charging on gas used would mean charging less than was paid.
- The minimum is one. Nothing costs zero units.
The same thing as code, runnable, with the constants taken from GET /v1/network:
/**
* The Relay Unit formula, schedule version 1 — computed on your side, before you sign.
*
* Integer arithmetic on fields of the transaction and on constants the network publishes; nothing
* else. `POST /v1/quote` returns the same number, and `GET /v1/network` returns the constants
* (`minGasLimit`, `gasPerDataByte`, `extraGasRelayed`, `extraGasGuarded`), so a client can check
* the service's arithmetic rather than trust it.
*/
/** 0.001 EGLD, in atto-EGLD. A contract constant: it has no setter. */
export const RU_SIZE_ATTO = 1_000_000_000_000_000n;
export interface NetworkConstants {
readonly minGasLimit: bigint; // erd_min_gas_limit
readonly gasPerDataByte: bigint; // erd_gas_per_data_byte
readonly extraGasRelayed: bigint; // erd_extra_gas_limit_relayed_tx
readonly extraGasGuarded: bigint; // erd_extra_gas_limit_guarded_tx
}
export interface Priced {
/** Gas that moves the transaction; the rest of the gas limit is processing gas. */
readonly moveGas: bigint;
/** The worst-case fee, in atto-EGLD: what the relayer is exposed to. */
readonly maxFee: bigint;
readonly ru: bigint;
}
export function relayUnits(
tx: {
readonly gasLimit: bigint;
readonly gasPrice: bigint;
readonly dataBytes: number;
readonly guarded: boolean;
},
net: NetworkConstants,
): Priced {
const moveGas =
net.minGasLimit +
net.gasPerDataByte * BigInt(tx.dataBytes) +
net.extraGasRelayed + // always: everything CoRelayer relays is relayed v3
(tx.guarded ? net.extraGasGuarded : 0n);
if (tx.gasLimit < moveGas)
throw new RangeError(`gasLimit ${tx.gasLimit} is below moveGas ${moveGas}`);
// The node applies a 0.01 price modifier with truncation; the schedule rounds UP, so it can
// never under-count what the relayer is exposed to — and needs no floating point.
const procPrice = (tx.gasPrice + 99n) / 100n;
const maxFee = moveGas * tx.gasPrice + (tx.gasLimit - moveGas) * procPrice;
const ru = maxFee <= RU_SIZE_ATTO ? 1n : (maxFee + RU_SIZE_ATTO - 1n) / RU_SIZE_ATTO;
return { moveGas, maxFee, ru };
}
/** The constants as `GET /v1/network` reports them. */
export function constantsOf(network: {
readonly minGasLimit: number;
readonly gasPerDataByte: number;
readonly extraGasRelayed: number;
readonly extraGasGuarded: number;
}): NetworkConstants {
return {
minGasLimit: BigInt(network.minGasLimit),
gasPerDataByte: BigInt(network.gasPerDataByte),
extraGasRelayed: BigInt(network.extraGasRelayed),
extraGasGuarded: BigInt(network.extraGasGuarded),
};
}
The named constants
moveGas is built from protocol constants referenced by name, not copied as numbers. The backend
re-reads them from the network on every epoch change, so a protocol change that alters the real
cost of a relayed transaction alters the Relay Unit count automatically — because the unit tracks
cost, that is the correct behaviour. GET /v1/network reports the values currently in use.
Their values on mainnet when the schedule was specified (2026-09-19):
| Constant | Value |
|---|---|
erd_min_gas_limit | 50,000 |
erd_gas_per_data_byte | 1,500 |
erd_extra_gas_limit_relayed_tx | 50,000 |
erd_extra_gas_limit_guarded_tx | 50,000 |
erd_gas_price_modifier | 0.01 |
erd_min_gas_price | 1,000,000,000 |
erd_max_gas_per_transaction | 600,000,000 |
One deliberate difference from the node: where the node truncates when it applies the price modifier, the schedule rounds up. For a gas price that is not a multiple of 100 the two differ by at most one atto per gas unit. Rounding up can never under-count what the relayer is exposed to, and it removes floating point from every client that wants to check the arithmetic.
Work it out
Pick the shape of a transaction and the card below runs the same formula, with every intermediate value shown. The shapes are rows of the golden-vector table that follows.
Work out what a transaction costs
Relay Unit schedule, version 1
A typical smart-contract call: a swap, a stake, a mint.
- Gas limit declared
- 5,000,000
- Data field
- 180 B
- Move gas
- 370,000
- Processing gas
- 4,630,000
- Gas price
- the network minimum
- Relay Unit size
- 0.001 EGLD
Move gas is charged at the full gas price; everything above it at one hundredth of it. The total is divided by the Relay Unit size and rounded up. The protocol constants were read from MultiversX on ; if MultiversX changes one, the count changes with it. No self-serve transaction may cost more than 25 Relay Units (15 on Starter and Agent Metered), and one over that limit is refused before you sign rather than billed.
Golden vectors
The single reference set for every implementation of the formula. The backend's implementation is tested against it, and this site's own test suite recomputes every row below from the formula above and fails the build if a single figure disagrees.
| Case | gasLimit | gasPrice | data bytes | guarded | moveGas | maxFee (atto) | RU |
|---|---|---|---|---|---|---|---|
| Plain EGLD transfer | 100,000 | 1,000,000,000 | 0 | no | 100,000 | 100,000,000,000,000 | 1 |
ESDTTransfer | 500,000 | 1,000,000,000 | 50 | no | 175,000 | 178,250,000,000,000 | 1 |
| Typical contract call | 5,000,000 | 1,000,000,000 | 180 | no | 370,000 | 416,300,000,000,000 | 1 |
| Exactly at the 1-RU boundary | 75,250,000 | 1,000,000,000 | 100 | no | 250,000 | 1,000,000,000,000,000 | 1 |
| One gas unit past it | 75,250,001 | 1,000,000,000 | 100 | no | 250,000 | 1,000,000,010,000,000 | 2 |
| 1 KB of data, 10 M gas | 10,000,000 | 1,000,000,000 | 1,024 | no | 1,636,000 | 1,719,640,000,000,000 | 2 |
| Maximum gas, small data | 600,000,000 | 1,000,000,000 | 100 | no | 250,000 | 6,247,500,000,000,000 | 7 |
| Guarded, cross-shard (a mainnet transaction) | 9,900,000 | 1,000,000,000 | 87 | yes | 280,500 | 376,695,000,000,000 | 1 |
| Gas price not a multiple of 100 | 5,000,000 | 1,234,567,891 | 180 | no | 370,000 | 513,950,613,440,000 | 1 |
| Self-serve worst case | 600,000,000 | 2,000,000,000 | 4,096 | yes | 6,294,000 | 24,462,120,000,000,000 | 25 |
| A purchase transaction (40 M gas) | 40,000,000 | 1,000,000,000 | 120 | no | 280,000 | 677,200,000,000,000 | 1 |
| Smallest round-up (one atto) | 5,000,000 | 1,000,000,001 | 180 | no | 370,000 | 416,300,005,000,000 | 1 |
The guarded row reproduces the fee of a real mainnet relayed transaction. The last row is the
round-up rule at its smallest: procPrice = (1_000_000_001 + 99) / 100 = 10_000_001, one more
than the node's truncated value.
Why "transactions" is a fair shorthand
In a sample of real MultiversX mainnet relayed v3 transactions taken while the schedule was being specified, 88.4 % were exactly one Relay Unit and the mean was 1.187. That is why plans are described in transactions, with the footnote that heavy transactions count as several — and why the footnote is always there rather than in small print. Those two figures describe the network's traffic, not CoRelayer's; CoRelayer has relayed nothing yet.
The thing that makes a transaction expensive is almost never what people expect. It is not the value being moved and it is not the contract being called. It is:
- an unnecessarily high gas limit — you are charged on what the network could take;
- a large data field — each byte adds to the movement cost at the full gas price;
- a raised gas price — it multiplies the movement part of the fee directly.
Check before you sign
POST /v1/quote computes the units for a transaction you have not signed yet. Its body carries the
transaction as tx (signed or not — only sender, gasLimit, gasPrice, data, options and
guardian are read) and, optionally, the account that would be billed:
curl -sS https://api.co-relayer.com/v1/quote \
-H 'content-type: application/json' \
-d '{"tx":{"sender":"erd1…","gasPrice":1000000000,"gasLimit":150000,"data":""}}'
The answer — a RelayQuote — carries ru, maxFeeAtto, moveGas, the schedule version, the
tariff, the pay-as-you-go price per unit and billedAs: how this transaction would be billed for
that account (grant, bonus, cap, payg, free, or none when it would not be served).
Amounts in it are decimal strings. POST /v1/validate goes further and returns every problem a real
relay would raise — without a lease, without a reservation and without asking the signer for
anything. (Check before you send)
Admission limits
Some transactions are refused before units are even counted, because they would expose a relayer to more than the service will risk on one transaction:
| Limit | Rule | Error |
|---|---|---|
| Gas price | between the network minimum and twice it; exactly the minimum on the free purchase flow | GAS_PRICE_OUT_OF_RANGE |
| Gas limit, lower bound | at least moveGas | GAS_LIMIT_TOO_LOW |
| Gas limit, upper bound | 100,000,000 on rate classes 1 and 6; 600,000,000 on classes 2 to 5 | GAS_LIMIT_TOO_HIGH |
| Data size | at most 4,096 bytes on a self-serve account | DATA_TOO_LARGE |
| Over-provisioned gas | with simulation on, gasLimit − moveGas at most 1.9 × the simulated processing gas | GAS_OVERPROVISIONED |
| Contract deployment or upgrade | allow-listed per account, then at most 65,536 bytes | DEPLOY_NOT_ALLOWED |
The sender can pay value | checked before co-signing — a transaction that fails for lack of funds still costs the relayer its whole fee | INSUFFICIENT_SENDER_BALANCE |
Together these fix the heaviest transaction a self-serve account can send: 15 Relay Units on classes 1 and 6, 25 on classes 2 to 5 (the "self-serve worst case" row above). The burst of every rate class is at least that large, so a transaction the class permits can always be admitted. (Limits)
Versioning
The formula is a published, versioned document, and the contract stores its version and a
SHA-256 hash of it (getRuSchedule). Two properties follow:
- a change to the schedule carries the same 48 hours of notice as a price increase;
- anyone can check that the schedule they read is the one the owner committed to on chain, by comparing the hash.
Caps are always denominated in units of the schedule in force at relay time. The size of a Relay Unit — 0.001 EGLD — is a contract constant with no setter at all: changing it would silently reprice every cap ever sold, so it is an upgrade with a migration, never a switch.
If a protocol upgrade ever adds a fee-relevant transaction field that the schedule does not price,
the service refuses transactions carrying it with
UNSUPPORTED_TX_FIELD until a schedule version that prices it is
in force.