relayInstantOperations
Submit pre-signed InstantLayer operations to the GaslessQ relayer — the explicit primitive underneath the transparent execution mode. Use it when you build and sign SignedOperation structs yourself (with buildSignedOperation + signSignedOperation) and want full control over batching and labels.
Relaying SDK actions? Prefer relayGaslessBatch. It takes the relayable
writes by name, mixes in GaslessWallet calls, and does the encoding, nonce sequencing, fee preview and signing this
primitive leaves to you. Reach for relayInstantOperations for operations the SDK does not describe — flex-field
flows or registered templates.
import { relayInstantOperations, waitForGaslessRequest } from "@symmio/trading-core";
const receipt = await relayInstantOperations(config, {
userAddress: owner,
operationType: "initiateWithdraw",
operations: [{ operation, signature }],
});
const record = await waitForGaslessRequest(config, { requestId: receipt.requestId });One POST {operationBase}/gateway/relay-instant. The service validates and simulates the batch, returns 202 with a request_id, and broadcasts asynchronously. The batch is atomic — one revert rolls back every operation and every fee — so only bundle operations that must share fate, and give them strictly sequential replay nonces (current + 1, current + 2, … from getInstantLayerNonce).
Every batch carries an explicit walletIds array — one decimal id per operation, taken from each entry’s walletId (default 0n). The service would zero-fill an omitted array for legacy clients, but the SDK never relies on that: a dropped id would then be indistinguishable from a deliberate wallet 0. Ordinary InstantLayer operations and delegation grants always relay from wallet 0; only a gasless-wallet execute names a positive id, and that id must match the wallet the operation was signed against.
Transport failures are dispositioned before they reach you. A 429 or a gateway 503 is resent under the same key — the gateway dropped it, so nothing can have executed. An ambiguous failure (no response, a timeout, a 502/504) is resent once, after a short backoff. If that resend does not land either, however it is answered, the submit is thrown as GASLESS_SUBMIT_UNCONFIRMED with the exact bytes to replay through resubmitGaslessRequest. A definitive 4xx is thrown as-is; inspect it with parseGaslessErrorDetail or classifyGaslessFailure.
A 202 is the point of no return: the workflow exists server-side and the transaction may execute even if polling
later fails. Never re-submit the same intent through the wallet afterwards — see the fallback safety
rules.
This action is a submit: it resolves with the 202 acceptance. React consumers do not need to follow it by hand —
the matching hook in @symmio/trading-react confirms the request before it resolves and invalidates the reads this
action changes. See the React gasless guide.
Parameters
operationsreadonly GaslessSignedOperationInput[]requiredThe signed operations in execution order — each { operation, signature }, with an optional walletId (default
0n) and optional fills / flexFillerSignatures for flex-field flows. An empty array throws
GASLESS_EMPTY_BATCH.
operationTypestringrequiredWorkflow label stored with the request (display, metrics, idempotency). Free-form, 1–128 chars — fees derive from the calldata selectors, never from this. Keep it stable per action.
userAddressAddressrequiredWallet the workflow is tracked under (not necessarily the raw signer).
accountIdstringoptionalClient accounting id stored with the request.
templateIdnumberoptionalRegistered result-chained template id; omit for independent batches. Must be a non-negative safe integer — an
unguarded NaN would serialize to JSON null, which the service reads as “no template” and relays as a plain
batch. The contract’s relayInstantTemplate takes no wallet ids, so every operation in a template batch must stay
on wallet 0.
idempotencyKeystringoptionalStable retry key (max 128 chars); defaults to a random UUID. One key per signature: reuse it only to replay the
byte-identical request, and mint a fresh one for any payload change — re-signing, adding an operation, moving a
wallet id — or the service answers 409 IDEMPOTENCY_KEY_CONFLICT. See idempotency
keys.
metadataRecord<string, unknown>optionalNon-secret client correlation data stored with the request.
chainIdnumberoptionalTarget chain id. Defaults to the config’s defaultChainId.
Returns
Promise<GaslessSubmitReceipt>requestIdstringStable tracking id — persist it immediately.
statusGaslessRequestStatusStatus at acceptance, normally queued.
paidFeebigint | nullThe fee the service quoted when it accepted the request, as reported by the service. A quote, not proof that a fee
was collected. null when the acceptance reported none.
remainingFeeAllowancebigint | nullThe service’s acceptance-time view of the payer’s remaining allowance, as reported by the service. Not a
post-execution balance or an authorization guarantee, and it can read 2^256 − 1. null when the acceptance
reported none.
idempotencyKeystringThe key the request was submitted with — minted here when the caller passed none. Persist it: resending the byte-identical request under this key is what makes a lost response recoverable.
protocolInstancestring | nullThe instance the request was accepted on, or null for a proxy base that names none.
ownerAddressThe owner the request is tracked under — the submitted userAddress.
walletIdsreadonly bigint[]The wallet ids the batch relayed under, in signedOps order.
Persist the whole receipt, not just the id. A bare request id is not enough context to reconcile a workflow after
a reload: store it under (environment, protocolInstance, service, requestId) together with owner, walletIds and
idempotencyKey. Only requestId is guaranteed on the wire — the parser keeps the acceptance even when its status is
unfamiliar (read as queued) or its amounts are missing (read as null), because losing the id to a strict parse
would strand a workflow that is already executing.
The receipt’s fee fields are acceptance-time figures, not results. The GaslessLayer collects the operational fee
only when the batch executes, from the payer resolved at that moment, so the charge can differ from paidFee, and a
reverted relay charges nothing. remainingFeeAllowance has read 2^256 − 1 on staging while the payer’s Core
approval was bounded, so never display it as spendable or unlimited funds. Read the payer’s allowance with
getOperationalFeeAllowance and its Core balance with
getAccountBalanceOf, both in 18-decimal Core units, and reconcile the
actual charge from the transaction receipt’s OperationalFeeRouted events. Treat both fields as informational: the
SDK passes them through as reported by the service.
Throws
SymmError("config", "GASLESS_NOT_CONFIGURED")/("config", "GASLESS_UNSUPPORTED_CONTRACTS_VERSION").SymmApiErrorwith codeGASLESS_RELAY_SUBMIT_FAILED— HTTP failure; the vendor’sdetail.code(FEE_POLICY_WOULD_REVERT,INSUFFICIENT_ALLOWANCE,SIMULATION_REVERTED, …) rides inresponseData.SymmError("api", "GASLESS_INSTANCE_MISMATCH")— routing cross-wire detected.SymmError("validation", "GASLESS_VALUE_UNSAFE")— a uint256 field exceeded the JSON safe-integer range.SymmError("validation", "GASLESS_EMPTY_BATCH")—operationswas empty.SymmError("validation", "GASLESS_WALLET_ID_INVALID")— awalletIdoutside0 … 2^256 − 1, or a JavaScriptnumber.SymmError("validation", "GASLESS_TEMPLATE_ID_INVALID")—templateIdwas not a non-negative safe integer.SymmError("validation", "GASLESS_TEMPLATE_WALLET_ID_UNSUPPORTED")— a template batch named a non-zero wallet id.SymmApiErrorwith codeGASLESS_SUBMIT_UNCONFIRMED— the outcome could not be established, including a202that carried norequest_id.responseDatais the replayableGaslessUnconfirmedSubmit; never re-run the intent through the wallet.SymmApiErrorwith codeGASLESS_INSTANCE_MISMATCHon a2xx— another protocol instance accepted it; the parsed body,request_idincluded, stays inresponseData.