Gasless Trading & Relayers
Eight actions on a pair can be signed instead of sent: the user signs an EIP-712 request in their
wallet, and a relayer submits it from its own account. The relayer pays the gas and the Pyth fee in
ETH; the pair credits it the relayerFee the user signed, in USDC, on its claimable balance. With a
USDC permit riding along, a user needs no ETH and no approve transaction at all.
| Relayable | Not relayable |
|---|---|
depositCollateral, withdrawCollateral, createOrder, cancelOrders, amendOrders, depositToInsurance, requestInsuranceWithdrawal, executeInsuranceWithdrawal | claimFees, executeTriggeredOrders, and everything outside the pair (approvals, termination, listings). |
Gasless calls also solve the 3-second price window for browser users: the price update is not part of what the user signs, so the relayer attaches a fresh one when it submits, however long the user took to approve.
This page covers both sides: signing on the frontend, and a complete relayer server. The protocol’s
rules are in Gasless Transactions. The
samples assume bz connected as in Connecting, user the wallet’s address and eth
a market.
How It Works
- The frontend calls
bz.relay.signRelayed(input, opts). It checks the action against the chain (as the direct builders do), signs a USDC permit first if asked, then has the wallet sign the request, and returns aRelayedRequest. - It posts
relay.serializeRelayed(req)(JSON) to the relayer, at once: the request is valid for at most 10 seconds. - The relayer parses it (
relay.parseRelayed), checks it (bz.relay.verifyRelayed), builds the call with a fresh price update and fresh book hints (bz.relay.encodeRelayed), and sends it from its own key. - The pair recovers the signer from the signature, acts as them, pulls the fee from their wallet (or nets it from a payout) and credits it to whoever submitted the transaction.
- The relayer collects its fees per pair with
claimFees.
Signing a Request
/** POST a signed request to the relayer at once: it is valid for at most 10 seconds. */
async function submit(req: relay.RelayedRequest): Promise<Hex> {
const res = await fetch(RELAYER_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: relay.serializeRelayed(req),
});
const body = (await res.json()) as { hash?: Hex; error?: string };
if (!res.ok || !body.hash) throw new Error(body.error ?? `the relayer answered ${res.status}`);
return body.hash;
}
try {
const block = await bz.ctx.chain.blockNumber();
const req = await bz.relay.signRelayed(
{
action: "createOrder",
order: {
orderType: OrderType.Limit,
isLong: true,
size: parseUnits18("0.5"),
limitPrice: parseUnits18("2950"),
expirationBlock: block + orders.blocksIn(24 * 60 * 60),
},
},
{
wallet: window.ethereum,
signer: user,
pair: eth.address,
relayerFee: parseUnits18("0.05"), // what this relayer charges, in USDC, at most $1
permit: true, // sign a USDC permit for the fee too, unless the allowance already covers it
},
);
const hash = await submit(req);
await bz.waitForReceipt(hash);
} catch (e) {
// A declined signature or a clock problem is a SigningError, whose message is the sentence to show.
show(e instanceof signing.SigningError ? e.message : errorMessage(e));
}
bz.relay.signRelayed(input, opts) takes what to do as input, one of:
input | Signs |
|---|---|
{ action: "createOrder", order } | An OrderInput (Placing Orders), checked against the chain as prepareOrder does, with the connection’s integrator. |
{ action: "amendOrders", amendments } | AmendInput[] (Managing Orders), checked and given hints as prepareAmends does. |
{ action: "depositCollateral", amount }, { action: "withdrawCollateral", amount }, { action: "depositToInsurance", amount } | An amount in BAZAAR precision. Deposits are floored to micro-USDC first. |
{ action: "requestInsuranceWithdrawal", shareAmount }, { action: "executeInsuranceWithdrawal" } | See Insurance. |
{ action: "cancelOrders", orderIds } | 1 to 200 distinct order ids. |
{ action: "createOrder", spec }, { action: "amendOrders", amends } | An already built OrderSpec, or the flat amendment words, signed as given. |
| Option | Default | Meaning |
|---|---|---|
wallet | required | The user’s EIP-1193 provider. |
signer | required | The user’s address. The wallet must sign as it. |
pair | required | The market’s pair. It is the domain the user signs under and the permit’s spender, so it must be one of the deployment’s pairs (InputError on pair otherwise, before anything is asked). |
relayerFee | 0n | What the relayer charges, BAZAAR precision, at most $1. Floored to micro-USDC before signing, so the signed fee is exactly what is charged. |
deadlineWindow | 10 | Seconds the signature stays valid, at most the deployment’s window (10). |
nonce | read | The nonce to sign. Read from the pair when omitted; pass it to sign several requests in a row. |
permit | none | true, or { value?, deadline?, always? }: also sign a USDC permit for what the call takes from the wallet. See Permits on the Gasless Path. |
What it does, in order:
- Floors the fee, reads the deployment’s fee cap and deadline window from the lens
(
eip712Limits, once per connection), and confirms the pair. - Resolves and checks the action: an order or amendments against the chain, a deposit’s floor, a
withdrawal on a terminated market against what it pays (
checkSignedFields, below). - Signs the permit, when asked, as its own wallet prompt. This happens before the request’s deadline is stamped, so the time the user spends on the permit prompt does not count against the 10 seconds.
- Reads the nonce and stamps the deadline (
chainDeadline). - Asks the wallet to sign the typed data with
eth_signTypedData_v4, normalizes the signature and recovers it locally: a signature that does not recover tosigneris refused (SigningError).
It returns a RelayedRequest: the signed fields (action and the action’s own arguments), pair,
chainId, signer, nonce, deadline (unix seconds), relayerFee (floored), signature and
permitData ("0x" without a permit).
const cancel = await bz.relay.signRelayed({ action: "cancelOrders", orderIds: [42n] }, { wallet: window.ethereum, signer: user, pair: eth.address });
const amend = await bz.relay.signRelayed(
{ action: "amendOrders", amendments: [{ orderId: 42n, limitPrice: parseUnits18("2960"), size: parseUnits18("0.5") }] },
{ wallet: window.ethereum, signer: user, pair: eth.address, relayerFee: parseUnits18("0.02"), permit: true },
);
const withdraw = await bz.relay.signRelayed(
{ action: "withdrawCollateral", amount: parseUnits18("50") }, // the fee comes out of the payout: no permit
{ wallet: window.ethereum, signer: user, pair: eth.address, relayerFee: parseUnits18("0.05") },
);
The Deadline Window
The pair accepts a signature only before its deadline and only if the deadline is at most 10 seconds
past the landing block’s timestamp (MAX_DEADLINE_WINDOW), so a signed request cannot be stored and
used later. That makes the timing tight:
- The deadline is counted from a checked clock: the lower of the latest block’s timestamp and the
device’s clock. If the two differ by more than 2 minutes (
MAX_CLOCK_SKEW),signRelayedthrowsClockSkewErrorand nothing is signed: an RPC reporting a block far in the future must not stretch a deadline. A device clock so far behind the chain that the deadline would already have passed by chain time is refused the same way. Local development chains (31337, 1337) use their own time unchecked. - The user has the window to approve the prompt. The deadline is stamped just before the wallet is
asked. If the user takes longer than the window, the request arrives expired: the relayer’s check
reports it (
deadline: expired 4 s ago) and the pair would refuse it (MetaTx__ExpiredDeadline). Ask the user to sign again; do not retry the same request. - A device clock behind the chain shortens the window: with the clock 4 seconds behind, a 10-second window leaves 6 seconds by chain time.
- Post at once, and the relayer must verify, encode and send at once.
bz.signing.chainDeadline(windowSeconds?) returns such a deadline for your own signing code, and
signing.checkClockSkew(chainNow, localNow?) the check alone.
Nonces
Each signer has one nonce per pair (metaTxNonces), shared by every action, strictly sequential: a
request lands only at the signer’s current nonce, and each landed request uses one.
signRelayedreads it (bz.signing.readMetaTxNonce(pair, user)) unless you passnonce.- To sign several requests before any lands, pass consecutive nonces and submit them in nonce order; the relayer must land each before the next is valid.
- A request whose nonce was used (another request landed first) fails with
MetaTx__InvalidNonce; sign it again.
// Two requests in a row: sign both against consecutive nonces, submit in nonce order.
const next = await bz.signing.readMetaTxNonce(eth.address, user);
const first = await bz.relay.signRelayed({ action: "cancelOrders", orderIds: [7n] }, { wallet: window.ethereum, signer: user, pair: eth.address, nonce: next });
const second = await bz.relay.signRelayed({ action: "cancelOrders", orderIds: [8n] }, { wallet: window.ethereum, signer: user, pair: eth.address, nonce: next + 1n });
await submit(first);
await submit(second);
Fees
The fee is the relayer’s price, agreed off chain: the frontend signs whatever the relayer charges, and
the relayer refuses requests below its price. The pair caps it at $1 per action (MAX_RELAYER_FEE,
checked against the lens’s maxRelayerFee) and charges it at its USDC floor (signing.floorRelayerFee).
Where it comes from depends on the action:
| Action | The signer’s wallet pays (relay.relayedPull) |
|---|---|
depositCollateral, depositToInsurance | The deposit plus the fee. |
createOrder | The fee plus the trigger order’s keeper bounty. |
cancelOrders, amendOrders, requestInsuranceWithdrawal | The fee. |
executeInsuranceWithdrawal | The fee on the first, locking call; nothing on the paying call, whose fee comes out of the payout. |
withdrawCollateral | Nothing: the fee comes out of the payout. On a terminated market whose payout ratio leaves 0, the fee comes from the wallet. |
checkSignedFields(fields, relayerFee, terminalPayout?) enforces the bounds that need no read, and
signRelayed and verifyRelayed both run it: deposits of at least 5 USDC that exceed the fee, a
withdrawal larger than the fee (or, on a terminated market, a payout larger than the fee), a non-zero
share amount, 1 to 200 distinct cancel ids, 1 to 32 well-formed amendments, and an order spec the order
rules accept. A withdrawal on a terminated market that would pay nothing is refused.
Permits on the Gasless Path
permit: true signs a USDC permit for exactly what the call takes from the wallet
(relayedPull, rounded up to USDC units), so a user who has never approved anything can deposit,
trade, amend and cancel with signatures alone:
// A first deposit with no ETH and no approval: the permit covers the deposit plus the fee.
const deposit = await bz.relay.signRelayed(
{ action: "depositCollateral", amount: parseUnits18("100") },
{ wallet: window.ethereum, signer: user, pair: eth.address, relayerFee: parseUnits18("0.05"), permit: true },
);
await submit(deposit);
permit | Meaning |
|---|---|
true | Sign a permit for the pull, unless the pair’s current allowance already covers it. |
{ value } | The permit’s value in USDC units, instead of the pull. |
{ deadline } | The permit’s expiry, unix seconds (default 30 minutes). |
{ always: true } | Sign one even when the allowance covers the pull. |
The allowance check matters because a permit sets the allowance to its value: one signed anyway would
replace a larger approval with this call’s amount. A withdrawal takes no permit (InputError on
permit). See USDC Permits for the rest of the rules, including signing
one permit at a time.
Smart-Contract Wallets Cannot Sign
The pair verifies with plain ECDSA (ECDSA.recover) and acts for whatever address the signature
recovers to. A smart-contract wallet (a Safe, an ERC-4337 account) signs through ERC-1271, whose
signature recovers to some other address or to none. signRelayed catches this: it recovers the
signature locally and throws SigningError (“this wallet does not sign as a plain account. Gasless
calls need an account’s own signature; send the transaction directly instead”). Smart accounts send
their calls directly, and can batch them: see Smart Accounts & Batching.
The Wire Format
relay.serializeRelayed(req) is JSON.stringify with every bigint as a decimal string:
{
"action": "cancelOrders",
"orderIds": ["42"],
"pair": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"chainId": 42161,
"signer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"nonce": "3",
"deadline": 1762612345,
"relayerFee": "50000000000000000",
"signature": "0x1b2c...",
"permitData": "0x"
}
A createOrder request carries spec (the OrderSpec, with its numbers as strings) instead of
orderIds; deposits and withdrawals carry amount, amendOrders amends, and
requestInsuranceWithdrawal shareAmount.
relay.parseRelayed(body) takes the JSON string (or an already parsed object) and returns a
RelayedRequest, checking every field’s type and shape (InputError naming the field otherwise:
"nonce: expected a non-negative integer"). It canonicalizes the signature into the one form the pair
accepts (65 bytes, v 27 or 28): a v of 0 or 1 and a 64-byte EIP-2098 signature are converted, and a
high-s signature is refused. It does not verify the signature; that is verifyRelayed.
Running a Relayer
A relayer is a small service with a funded key: ETH for gas and Pyth fees on the deployment’s chain. It
earns USDC per request. The complete server below uses Node’s own node:http; any HTTP framework works
the same way.
Setup
const RPC_URL = process.env.RPC_URL ?? "http://127.0.0.1:8545";
const FACTORY = (process.env.BAZAAR_FACTORY ?? "0x0000000000000000000000000000000000000000") as Address;
const PORT = Number(process.env.PORT ?? 8787);
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN ?? "https://app.example.com";
/** The least this relayer accepts per request, and the most Pyth fee it will pay for one. */
const MIN_FEE = parseUnits18("0.02");
const MAX_PYTH_FEE = parseEther("0.0005");
const account = privateKeyToAccount(process.env.RELAYER_PRIVATE_KEY as Hex, { nonceManager });
const wallet = createWalletClient({ account, chain: arbitrum, transport: http(RPC_URL) });
const bz = await Bazaar.connect({
transport: RPC_URL,
factory: FACTORY,
chainId: arbitrum.id,
prices: { token: process.env.PYTH_PRO_API_KEY }, // the relayer attaches fresh prices; a server may hold the key
});
// Refuse to start if what users sign is not what this deployment verifies.
const typehashes = await bz.signing.verifyTypehashes();
if (!typehashes.ok) throw new Error(`EIP-712 mismatch: ${typehashes.mismatches.join("; ")}`);
bz.signing.verifyTypehashes(pair?) compares every type string the SDK signs with the lens’s
getTypehashes(), the domain’s name and version hashes and the fee and deadline caps with
getEip712Constants(), and, given a pair, the domain separator with the pair’s DOMAIN_SEPARATOR(). It
returns { ok, mismatches, typehashes, constants, domain? }. Run it once at startup.
Verifying
interface Answer {
status: number;
body: { hash?: Hex; error?: string; problems?: readonly { field: string; message: string }[] };
}
async function relayOne(body: string): Promise<Answer> {
let req: relay.RelayedRequest;
try {
req = relay.parseRelayed(body); // shape and types; the signature canonicalized
} catch (e) {
return { status: 400, body: { error: e instanceof InputError ? e.message : "expected a JSON request" } };
}
if (req.relayerFee < MIN_FEE) {
return { status: 402, body: { error: `this relayer charges at least ${formatUnits18(MIN_FEE)} USDC per request` } };
}
const check = await bz.relay.verifyRelayed(req); // signer, chain, deadline, nonce, fee cap, bounds, permit
if (!check.ok) return { status: 422, body: { error: "the request would revert", problems: check.problems } };
for (const w of check.warnings) console.warn(`${req.signer}: ${w}`);
if (check.secondsLeft < 2) return { status: 422, body: { error: "the signature expires too soon: sign again" } };
// Built now: a fresh price update and fresh book hints, outside the signature.
const call = await bz.relay.encodeRelayed(req, { relayer: account.address });
if (call.value > MAX_PYTH_FEE) return { status: 503, body: { error: "the price update fee is unusually high; try again" } };
const sim = await bz.simulate(call);
if (!sim.ok) return { status: 422, body: { error: sim.error!.message } };
try {
const hash = await wallet.sendTransaction({ to: call.to, data: call.data, value: call.value });
console.log(`${call.summary}: ${hash}`);
return { status: 202, body: { hash } };
} catch (e) {
return { status: 502, body: { error: errorMessage(e) } };
}
}
bz.relay.verifyRelayed(req) checks everything that would make the call revert, and returns a
RelayCheck instead of throwing:
| Field | Meaning |
|---|---|
ok | Safe to submit now: problems is empty. |
signer | The address the signature recovers to, or null. |
problems | { field, message } for each thing that would revert: the pair is not the deployment’s (nothing else is read then), the chain id, a signature that does not recover to signer over exactly these fields or is not in canonical form, a deadline that has passed or is more than 10 seconds ahead of chain time, a nonce that is not the signer’s current one (used, or ahead: “an earlier request has to land first”), a fee above the cap, and the action’s own bounds. |
warnings | What would not revert by itself: a permit that would fail (wrong signer, used nonce, expired, too small, bad v or s), after which the call needs an allowance already in place; a terminated-market withdrawal that pays nothing, whose fee then comes from the wallet. |
secondsLeft | Seconds until the deadline by chain time (negative once passed). |
permits.checkPermit(ctx, permitData, { owner, spender, needUsdc }) is the permit part on its own,
returning { ok, problems, value }.
Encoding and Sending
bz.relay.encodeRelayed(req, opts?) builds the PreparedCall the relayer sends: the signed fields,
the signature and permit, plus what sits outside the signature, built now:
- a fresh price update for the actions that take one (
withdrawCollateral,depositToInsurance,executeInsuranceWithdrawal,createOrder,amendOrders), withvalueits Pyth fee, which the relayer pays.createOrderandamendOrdersalways carry one, so the relayer’s connection needs a price source; - fresh
MatchHintsforcreateOrder(oropts.hints). A relayed order always walks the protocol’s default budget. Amendment hints are inside the signed words and cannot change.
| Option | Meaning |
|---|---|
relayer | The relayer’s address, used as the call’s from (so bz.simulate(call) runs as the relayer). Without it from is null. |
hints | MatchHints for a createOrder, instead of the ones the SDK finds. |
The call’s pullsUsdc is what it takes from the signer’s wallet, and its summary reads
Relay for 0x...: Buy 0.5 at 2950 (limit, GTC) (fee 0.05 USDC). It throws InputError for a request
signed for another chain or a signature not in canonical form (parseRelayed produces the canonical
form).
Send it at once, from the relayer’s own key, as on Sending From a Bot. Simulate first: the relayer pays the gas of a call that reverts.
Ordering
// A signer's requests on one pair land only in nonce order, so handle them one at a time, each after
// the previous one has been mined. Different signers run in parallel.
const queues = new Map<string, Promise<unknown>>();
function inOrder<T>(key: string, job: () => Promise<T>): Promise<T> {
const prev = queues.get(key) ?? Promise.resolve();
const run = prev.then(job, job);
const tail = run.catch(() => undefined);
queues.set(key, tail);
void tail.then(() => {
if (queues.get(key) === tail) queues.delete(key);
});
return run;
}
async function relayInOrder(body: string): Promise<Answer> {
let key = "malformed";
try {
const o = JSON.parse(body) as { pair?: unknown; signer?: unknown };
key = `${String(o.pair).toLowerCase()}:${String(o.signer).toLowerCase()}`;
} catch {
// relayOne answers 400
}
return inOrder(key, async () => {
const answer = await relayOne(body);
if (answer.body.hash) await bz.waitForReceipt(answer.body.hash, { timeoutMs: 30_000 }).catch(() => undefined);
return answer;
});
}
Requests from one signer on one pair can only land in nonce order, so the server handles them one at a
time and waits for each to be mined before the next. Requests from different signers do not wait for
each other. The relayer’s own transaction nonces are handled by viem’s nonceManager.
The Server
const MAX_BODY = 16 * 1024;
const perIp = new Map<string, { count: number; since: number }>();
function rateLimited(ip: string): boolean {
const now = Date.now();
const e = perIp.get(ip);
if (!e || now - e.since > 60_000) {
perIp.set(ip, { count: 1, since: now });
return false;
}
return ++e.count > 30; // 30 requests a minute per address
}
function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let size = 0;
const chunks: Buffer[] = [];
req.on("data", (c: Buffer) => {
size += c.length;
if (size > MAX_BODY) {
reject(new Error("body too large"));
req.destroy();
} else chunks.push(c);
});
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
function send(res: ServerResponse, status: number, body: unknown): void {
res.writeHead(status, {
"content-type": "application/json",
"access-control-allow-origin": ALLOWED_ORIGIN,
"access-control-allow-headers": "content-type",
});
res.end(status === 204 ? undefined : JSON.stringify(body));
}
createServer(async (req, res) => {
if (req.method === "OPTIONS") return send(res, 204, {});
if (req.method !== "POST" || req.url !== "/relay") return send(res, 404, { error: "POST /relay" });
if (rateLimited(req.socket.remoteAddress ?? "?")) return send(res, 429, { error: "slow down" });
try {
const answer = await relayInOrder(await readBody(req));
send(res, answer.status, answer.body);
} catch (e) {
console.error(e);
send(res, 500, { error: "the relayer failed; try again" });
}
}).listen(PORT, () => console.log(`relaying as ${account.address} on :${PORT}`));
Limit body size and request rate: a request costs the relayer chain reads before it can be refused. Set
access-control-allow-origin to your frontend’s origin.
Claiming the Fees
// Every hour, collect the fees earned on each market (claimFees refuses when nothing is owed).
async function claimAll(): Promise<void> {
for (const m of await bz.markets.listMarkets({ refresh: true })) {
try {
const call = await bz.trade.claimFees(m.address, account.address);
const sim = await bz.simulate(call);
if (!sim.ok) continue;
const hash = await wallet.sendTransaction({ to: call.to, data: call.data, value: call.value });
await bz.waitForReceipt(hash);
console.log(`${m.label}: ${call.summary}`);
} catch (e) {
if (!(e instanceof InputError)) console.error(`${m.label}: ${errorMessage(e)}`);
}
}
}
setInterval(() => void claimAll(), 60 * 60 * 1000);
Fees accrue to the relayer’s claimable balance on each pair, alongside any integrator fees if the
relayer’s address is also the frontend’s integrator. claimFees pays it all out and has no gasless
form (Managing Orders).
Lower-Level Pieces
The signing namespace has everything signRelayed is built from, for a relayer in another language or
a custom signer:
| Export | What it is |
|---|---|
TYPE_STRINGS, BAZAAR_TYPES | The eight primary types, as strings and as viem / eth_signTypedData_v4 field lists. |
pairDomain(chainId, pair), domainSeparator(chainId, pair) | The pair’s EIP-712 domain ("BazaarPair", version "1") and its separator. |
typedDataFor(chainId, pair, fields, meta) | The typed data a user signs. typedDataJson(td) is its JSON for eth_signTypedData_v4; relayedDigest(td) its digest. |
typeHash(primaryType) | keccak256 of a type string. |
signTypedDataV4(wallet, signer, td) | Sign through a wallet, normalized and checked to recover to signer. |
normalizeSignature(sig) | A signature in the 65-byte, v 27/28, low-s form the pair accepts. |
encodePairWrite(fields, extra, meta) | The calldata of any of the eight actions, direct or relayed. |
RELAYED_ACTIONS, PRIMARY_TYPE, takesPriceUpdate(action) | The relayable actions, their primary types, and which take a price update. |
MAX_RELAYER_FEE, MAX_DEADLINE_WINDOW, MAX_CLOCK_SKEW, DEV_CHAIN_IDS | $1, 10 s, 120 s, [31337, 1337]. |
In the relay namespace: describeRelayed(fields) (one line for a request), relayedPull and
checkSignedFields (above).
What Can Go Wrong
| You see | It means |
|---|---|
SigningError: “You declined the signature.” | The user rejected the prompt. |
SigningError: “… does not sign as a plain account …” | A smart-contract wallet. Send directly instead. |
ClockSkewError | The device’s clock and the RPC’s chain time disagree by more than 2 minutes. |
InputError on relayerFee: “A relayer fee is at most $1 per action.” | The fee cap. |
InputError on relayerFee: “The relayer fee must be less than the deposit.” | A deposit must exceed its fee. |
MetaTx__ExpiredDeadline / deadline: expired ... | The request took longer than its window. Sign again. |
MetaTx__DeadlineTooFar | A deadline more than 10 seconds ahead of the block. |
MetaTx__InvalidNonce / nonce: ... is already used | Another request from this signer landed first. Sign again. |
nonce: ... is ahead of the signer's next nonce | An earlier request has to land first. |
MetaTx__InvalidSignature | The signature does not match the request (a field changed after signing). |
MetaTx__RelayerFeeExceedsMax | The fee is above the deployment’s cap. |
BazaarPair__AmountLteRelayerFee | A withdrawal’s payout does not cover the fee. |
permit: ... warning | The permit will fail; the call then needs an allowance in place. |
Related Pages
- Collateral, Approvals & Permits: permits in detail.
- Placing Orders and Managing Orders: the actions being signed.
- Errors & Messages: showing these errors.
- Security Notes for Integrators.