Skip to main content

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:

relay-units.ts
/**
* 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):

ConstantValue
erd_min_gas_limit50,000
erd_gas_per_data_byte1,500
erd_extra_gas_limit_relayed_tx50,000
erd_extra_gas_limit_guarded_tx50,000
erd_gas_price_modifier0.01
erd_min_gas_price1,000,000,000
erd_max_gas_per_transaction600,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.

Costs1Relay Unit
Maximum network fee0.000416EGLD
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.

CasegasLimitgasPricedata bytesguardedmoveGasmaxFee (atto)RU
Plain EGLD transfer100,0001,000,000,0000no100,000100,000,000,000,0001
ESDTTransfer500,0001,000,000,00050no175,000178,250,000,000,0001
Typical contract call5,000,0001,000,000,000180no370,000416,300,000,000,0001
Exactly at the 1-RU boundary75,250,0001,000,000,000100no250,0001,000,000,000,000,0001
One gas unit past it75,250,0011,000,000,000100no250,0001,000,000,010,000,0002
1 KB of data, 10 M gas10,000,0001,000,000,0001,024no1,636,0001,719,640,000,000,0002
Maximum gas, small data600,000,0001,000,000,000100no250,0006,247,500,000,000,0007
Guarded, cross-shard (a mainnet transaction)9,900,0001,000,000,00087yes280,500376,695,000,000,0001
Gas price not a multiple of 1005,000,0001,234,567,891180no370,000513,950,613,440,0001
Self-serve worst case600,000,0002,000,000,0004,096yes6,294,00024,462,120,000,000,00025
A purchase transaction (40 M gas)40,000,0001,000,000,000120no280,000677,200,000,000,0001
Smallest round-up (one atto)5,000,0001,000,000,001180no370,000416,300,005,000,0001

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:

LimitRuleError
Gas pricebetween the network minimum and twice it; exactly the minimum on the free purchase flowGAS_PRICE_OUT_OF_RANGE
Gas limit, lower boundat least moveGasGAS_LIMIT_TOO_LOW
Gas limit, upper bound100,000,000 on rate classes 1 and 6; 600,000,000 on classes 2 to 5GAS_LIMIT_TOO_HIGH
Data sizeat most 4,096 bytes on a self-serve accountDATA_TOO_LARGE
Over-provisioned gaswith simulation on, gasLimit − moveGas at most 1.9 × the simulated processing gasGAS_OVERPROVISIONED
Contract deployment or upgradeallow-listed per account, then at most 65,536 bytesDEPLOY_NOT_ALLOWED
The sender can pay valuechecked before co-signing — a transaction that fails for lack of funds still costs the relayer its whole feeINSUFFICIENT_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.