Skip to Content
Symmio Trading-SDK — the SDK surface for builders on Arbitrum
CoreGaslessgetGaslessRequest

getGaslessRequest

Fetch one stored GaslessQ relayer request — the poll target of every gasless workflow. A relay submit returns 202 immediately; this record is the only source of truth afterwards.

import { getGaslessRequest, GaslessRequestStatus, isGaslessRequestTerminal } from "@symmio/trading-core"; const request = await getGaslessRequest(config, { requestId }); if (request.status === GaslessRequestStatus.SUBMITTED && request.txHash) { showExplorerLink(request.txHash); } if (isGaslessRequestTerminal(request.status)) stopPolling();

One GET {base}/{request_id} against the chain’s gasless service. Operation relays live on the "operations" sub-service (the default); deposit settlements on "deposits" — the two are separate databases, so a request id polled against the wrong one is NOT_FOUND.

submitted is not success — it only means a transaction hash exists. Only succeeded is success; reverted, failed and rejected are failures the record carries errorCode / errorMessage for. Right after a 202 the record may briefly 404 (the accept-vs-record race) — retry a few times before treating it as unknown, and never treat a post-accept 404 as “the relay is unavailable”.

The query factory polls on its own. A relay request is a workflow, not a value: fetching it once answers queued and freezes there, which reads as a stalled UI even though the relayer finished seconds later. So the factory ships the service-recommended cadence — a jittered 1–2 s while queued, 3–5 s after submitted, and stop once terminal, because a terminal record is immutable and polling one forever bills the service for an answer that cannot change.

// Polls to a terminal status, then stops. No cadence to hand-write. const request = useQuery(getGaslessRequestQueryOptions(config, { requestId }));

It also absorbs the two failures that are not answers — the accept-vs-record 404 right after a 202 (up to three), and transient transport failures (429, 503, a dropped connection) which are retried with the gateway’s Retry-After when it sent one and a jittered 1 s → 30 s backoff otherwise. A definitive answer (401, 403, 422, a service error code) is surfaced immediately, and a terminal record is treated as permanently fresh so a remount does not refetch it. Every default applies only where you supplied nothing, so query.refetchInterval, query.retry and query.staleTime still win.

The factory’s queryFn also forwards TanStack’s own AbortSignal, so a query that unmounts or is cancelled stops its poll in flight instead of finishing into a discarded result. Pass signal yourself outside React.

Polling helpers

gaslessPollDelay(status, random?)

The cadence the query factory uses, exported so a hand-rolled loop can share it: a jittered delay inside the service’s recommended band (1–2 s while queued, 3–5 s after submitted), or false once the status is terminal. The jitter is load-bearing on the anonymous gateway — every workflow a page opened at the same moment would otherwise poll in lockstep and spend the shared per-IP budget in bursts.

import { gaslessPollDelay } from "@symmio/trading-core"; const delay = gaslessPollDelay(request.status); if (delay !== false) setTimeout(poll, delay);

It draws a new number on every call, which is what a sleeping loop wants and what a TanStack refetchInterval callback must not do: TanStack recomputes that callback on every render and restarts the countdown whenever the value changed, so a re-drawn delay in a component that re-renders faster than the band never fires — the request freezes at queued. Let getGaslessRequestQueryOptions supply the interval; it seeds the draw from the query’s own fetch timestamps, so the value is stable between renders and fresh after every poll.

isNewerGaslessRequest(prev, next)

The stale-response guard for anything that writes a polled record somewhere shared — a query cache, a progress state, a stored row. Status reads are concurrent by construction: a poll, a retry after a transient failure and a relay’s own wait can all be in flight at once, and they do not answer in the order they were sent. Without this guard a slow queued response that lands after the succeeded one repaints a finished workflow as pending, which is exactly the “it looks stuck, submit it again” bug the lifecycle contract forbids.

The order is the lifecycle first — queued < submitted < terminal, with terminal sticky, because a terminal record is immutable — then updatedAt within one rank. When either record carries no usable timestamp the newer fetch wins, since a record with no timestamp cannot be proven stale.

import { isNewerGaslessRequest } from "@symmio/trading-core"; queryClient.setQueryData<GaslessRequest>(key, (previous) => isNewerGaslessRequest(previous, incoming) ? incoming : previous, );

Status reads are also paced internally, at about four requests per second per deployment (origin + protocol instance), with a burst of four. The anonymous gateway allows ten requests per second per IP and instance, and status polling is the one gasless call that runs unattended and in parallel — the pacing keeps a submit, a fee quote or a deposit-policy read from being the call that earns the 429.

Polling a request by hand is the low-level path. To simply wait for an outcome, use confirmGaslessRequest — or, in React, nothing at all: the relay hooks confirm before they resolve.

Parameters

requestIdstringrequired

Stable service tracking id returned by a gasless submit. Persist it — there is no list-by-wallet endpoint to recover lost ids from.

service"operations" | "deposits"default "operations"optional

Which sub-service stores the request. Operation relays and delegation grants live on "operations"; deposit settlements on "deposits".

signalAbortSignaloptional

Cancel the read in flight. The React hook passes TanStack’s own query signal, so an unmounted poll stops spending the deployment’s shared request budget.

chainIdnumberoptional

Target chain id. Defaults to the config’s defaultChainId. Selects which chain’s gasless service is polled, and is folded into the query key.

Returns

GaslessRequest is a per-service union: the two services are separate databases with different columns, so the shared fields live on the base shape and each service’s own fields on its variant. Narrow on service before reading a variant field.

const request = await getGaslessRequest(config, { requestId, service: "deposits" }); if (request.service === "deposits") { showSweep(request.depositAddress, request.walletId, request.creditedRaw); }
Promise<GaslessRequest>
service"operations" | "deposits"

Which service stores the record — the union’s discriminant.

requestIdstring

The stable tracking id, echoed back.

statusGaslessRequestStatus

queued → submitted → succeeded | reverted | failed, or queued → rejected (the worker refused it before any broadcast).

txHashHash | null

Broadcast transaction hash once the worker has submitted one. A hash is mutable across replacement attempts — the request id is the stable identifier.

errorCodestring | null

Vendor error code on a failed terminal (FEE_POLICY_WOULD_REVERT, SIMULATION_REVERTED, …), when stored.

errorMessagestring | null

Human-readable vendor message, when stored.

idempotencyKeystring | null

The retry key the request was submitted with, when stored.

ownerAddress | null

The owner the request is tracked under — user_address for an operation relay, wallet_address for a deposit settlement (which means the owner, not a GaslessWallet address).

walletIdsreadonly bigint[]

The wallet ids the request selected, in signedOps order. Records written before multiple wallets existed carry none and normalize to one 0n per stored operation, because an omitted id means wallet 0.

createdAtstring | null

When the service stored the request (ISO-8601), when recorded.

updatedAtstring | null

When the service last updated it (ISO-8601), when recorded.

service: "operations"

GaslessOperationRequest
operationTypestring | null

The workflow label the submitter attached.

accountIdstring | null

The client accounting id the submitter attached, when stored.

feeAmountRawbigint | null

The fee the service recorded, in the unit the service reports it in — a vendor figure, not an authoritative on-chain charge.

service: "deposits"

GaslessDepositRequest
depositAddressAddress | null

The deterministic deposit address being swept, when stored.

walletIdbigint

The settled wallet’s id. A record without one means wallet 0.

accountNamestring | null

Name of the sub-account a new-account settlement creates.

amountRawbigint | null

Collateral the service observed at the deposit address (token base units).

feeRawbigint | null

The deposit fee the service recorded (token base units).

creditedRawbigint | null

The net credit the service recorded (token base units) — an estimate, not the final credit.

Throws

  • SymmError("config", "GASLESS_NOT_CONFIGURED") / ("config", "GASLESS_UNSUPPORTED_CONTRACTS_VERSION") — the chain has no usable gasless service.
  • SymmApiError with code GASLESS_STATUS_FETCH_FAILED — HTTP failure, including the post-submit 404 race.
  • SymmError("api", "GASLESS_INSTANCE_MISMATCH") — a successful response came from a different protocol instance than the config expects (protocolInstance, or the instance the url path pins): a staging/production cross-wire. A gateway url also fails when the X-GasLessQ-Protocol-Instance header is missing; behind a proxy root, only a forwarded header is checked. See URL forms.
  • SymmError("api", "GASLESS_STATUS_UNKNOWN") — the service returned a status outside the documented lifecycle.
Last updated on