Credits and billing
There are four money-shaped things in CoRelayer, and they behave differently. Keeping them apart is most of understanding the billing.
| What it is | Created by | Reversible | |
|---|---|---|---|
| A deposit | USDC arriving at the contract | You, signing a transfer | No — it becomes credits in the same call |
| Credits | A USDC-denominated balance inside the contract | A deposit | No |
| A plan block | The record of what you bought, with its terms frozen | Spending credits on a tier | No |
| Escrow | Credits set aside for pay-as-you-go usage | Opting into pay-as-you-go | Back to credits — never to your wallet — once you turn pay-as-you-go off |
Depositing
A deposit is an ESDT transfer of USDC to the contract that also calls one of three functions:
| Function | Effect |
|---|---|
deposit() | Credits your own account. |
depositFor(beneficiary) | Credits someone else's account. |
depositAndSubscribe(tier, months, max_price, ref) | Deposits and buys a plan, in one transaction. |
The contract swaps the USDC in the same call and credits you 1:1 from the USDC you paid — not from what the swap returned. (Where the money goes)
| Bound | Launch value |
|---|---|
| Minimum deposit | 1 USDC. It exists because a deposit costs a relayer gas, so a dust deposit would cost more to process than it is worth. It is deliberately not scaled by the tariff. |
| Maximum deposit | Unlimited at launch. If the owner ever sets a ceiling, it bounds both a single call and the total swapped per minute, and the API refuses over-large deposits before co-signing rather than letting the transaction revert. |
If the swap venue is paused, or the pair or token is unavailable, the whole deposit reverts and you
keep your USDC. The API sees this coming and answers
SWAP_VENUE_PAUSED instead of relaying a transaction that would
certainly fail — which matters because on the free purchase flow, our relayer pays for that
failure.
Buying with a price ceiling
No purchase path exists without a ceiling you set yourself. Every purchasing call takes a
max_price, and the contract reverts if the price at execution time is above it. This includes
adding named wallets, which also costs Relay Units. A sponsor key costs nothing beyond the Relay
Units it relays.
The flow the dashboard and the SDK use:
POST /v1/subscribe/preparereturns a quote and the unsigned transaction.- You sign the transaction. Its arguments — tier, months,
max_price, and a reference — are part of the signed bytes, so what you approved is what executes. - The contract prices the purchase at the block timestamp and reverts if that exceeds your
ceiling:
PRICE_ABOVE_MAX.
{
"quoteId": "q_01JA7M3Z9K",
"tierId": 12,
"months": 1,
"tariff": "10000",
"tariffVersion": 1,
"priceMicroUsdc": "85000000",
"maxPrice": "85000000",
"pendingTariff": null,
"issuedAtMs": 1789819200000,
"expiresAtMs": 1789819320000
}
Three properties of quotes worth knowing:
- They live for 120 seconds, and they are stateless. A quote is not a row in a table we could lose or mismatch across hosts; it is content plus a signature over that content, so any host can verify one it did not issue. Asking twice simply gives you two valid quotes.
maxPriceequals the price. There is no built-in slack. If the price goes down between quote and block, you are charged less.- A quote never straddles a price increase silently. If an increase becomes effective before the quote expires, the quote is priced at the pending, higher value and says so. A transaction that lands before the activation pays the lower current price and the surplus stays as credits. You are never charged more than you accepted, and the purchase never fails for this reason.
What a purchase freezes
Paying writes a plan block, and a plan block is immutable for its term. It records:
- the cap in Relay Units and the length of the period;
- the pay-as-you-go price per unit;
- the number of named wallets (listed senders) allowed;
- the rate class and service class;
- the tariff version it was priced at.
Nothing later can change it. Raising the tariff does not; editing the tier does not; retiring the tier does not. An account holds at most one current block and one queued block, so buying ahead does not disturb what is running.
Prepaying, upgrading and downgrading
| Prepaying | Buy 1 to 12 months in one purchase. Every month is priced at the tariff in force when the purchase executes, and that price holds for the whole block. There is no discount for prepaying: the benefit is the locked price. |
| Unused transactions | They belong to the period they were bought for and expire with it. Nothing rolls over into the next period. |
| Upgrading mid-period | A tier with a higher price starts now. The months of the old block that had not started are credited back to your credits, and the unused part of the current period's cap carries over once, into the first period of the new block. The started period itself is not refunded. |
| Downgrading | There is no immediate downgrade. A lower tier starts when your paid block ends: buy it as the queued block, or set it as your auto-renew tier. A prepaid block cannot be shortened or swapped for a cheaper one. |
A credit-back is not a refund: it returns to your credits, which buy plans and pay-as-you-go usage and are not converted back to USDC.
Renewal
| Manual | Buy again. The new block queues behind the current one. |
| Auto-renew | setAutoRenew(enabled, tier, max_renew_price) — and max_renew_price must be greater than zero when enabled. |
Zero never means "unlimited". An account that renews itself has to state its ceiling explicitly; that is a deliberate refusal to offer an open-ended standing authorisation, and it matters most for autonomous buyers, which are exactly the accounts most likely to renew unattended.
Renewal is permissionless: anyone may trigger the renewal of an account that has opted in, because the terms are fixed by the account itself and the contract enforces them. This keeps renewals working without a scheduler that has to be running at midnight.
When there is no entitlement
| Situation | What you get |
|---|---|
| No plan at all | NO_ENTITLEMENT — 402, with a pointer to the pricing document and the x402 endpoint. |
| Cap used up, pay-as-you-go off | QUOTA_EXHAUSTED — 429 with no Retry-After, because waiting does not help. |
| Cap used up, pay-as-you-go on, escrow empty | QUOTA_EXHAUSTED with details.reason = PAYG_ESCROW_EMPTY — again without Retry-After. |
Pay-as-you-go price above your max_payg_price | PAYG_PRICE_ABOVE_MAX. |
| A purchase from credits, and there are not enough | INSUFFICIENT_CREDITS — deposit first, or buy with depositAndSubscribe. |
| Suspended | ACCOUNT_SUSPENDED. |
Escrow, and how it comes back
Opting into pay-as-you-go moves credits into escrow: an amount set aside for usage beyond the cap. Only settled pay-as-you-go usage can spend it, and nothing else can spend it — that separation is what stops the same credits being spent twice, once on a plan and once on usage.
What pay-as-you-go does not use comes back to your credits — not to your wallet; credits are never refundable — once you turn pay-as-you-go off:
- Turn it off.
setPayg(false, …)stops new pay-as-you-go usage and starts the close. The escrow stays locked for now, because usage from before the switch may still be settling. - The normal release. Once every pay-as-you-go transaction from before the switch is settled, and at least an hour after it, the settlement key sends a closing line that returns the rest to your credits.
- The fallback. If that never happens — the settlement key dead, or hostile — anyone may call
releaseEscrow(account)seven days after the switch, and the escrow returns to that account's credits. When the account itself sends it, CoRelayer relays it free of charge. No pause scope blocks this endpoint: a deliberate constraint on ourselves, so that no switch of ours can lock it.
Lowering the pay-as-you-go budget never releases escrow; only turning pay-as-you-go off does.
Who pays for a given transaction
When a transaction arrives, the payer is resolved in a fixed order:
- a sponsor API key, if one was presented;
- the account explicitly named in the request, if that account authorised this sender on chain;
- the sender's own account;
- the oldest account that authorised this sender;
- the free list — a short, published set of calls to our own contract that CoRelayer pays for, so that buying and managing a plan never requires already having one.
If none of these applies you get NO_ENTITLEMENT, SENDER_NOT_AUTHORIZED or QUOTA_EXHAUSTED,
depending on which step failed.
Reading your own numbers
GET /v1/account/{erd} | Plan, credits, flags — a mirror of chain state, public. |
GET /v1/account/{erd}/quota | Units used, cap, period end. |
GET /v1/account/{erd}/purchases | Every purchase, from the contract's own events. |
GET /v1/usage, GET /v1/usage/summary | Per-transaction and aggregated usage. Private. |