Skip to content

Trading & quotes API

The core trading surface. Mounted at /api/defi.

Route file: bz-backend/routes/defi.routes.js. Controllers: controllers/defi/swap.js, controllers/defi/transaction.controller.js, controllers/defi/init.js.

GET /defi/quotes — SSE quote stream

Fans out to every registered aggregator and streams each result as it arrives.

Query parameters

ParamRequiredNotes
fromChainIdyesNetwork.id
toChainIdyesEqual to fromChainId for a swap
fromTokenyesAddress; native sentinels resolved
toTokenyesAddress
amountyesHuman units (1.5, not wei)
typeyesswapSWAP, anything else → BRIDGE
srcWalletAddressQuote context
dstWalletAddressWhere output must land
slippageDefault 0.5 (percent)
anonymousMode'true' restricts the fan-out to Houdini only

anonymousMode is a real feature

When anonymousMode === 'true', the controller sets both excludeSwap and excludeBridge to every aggregator except Houdini — the privacy-oriented provider. It is an opt-in privacy route, not a debug flag.

Response

text/event-stream, Cache-Control: no-cache, Connection: keep-alive.

data: {"id":"…uuid…","aggregator":"lifi","amount":"4231120000", …}\n\n
data: {"id":"…uuid…","aggregator":"odos", …}\n\n

Stream ends when all aggregators settle. Consume with EventSource.

Errors

404 with the standard envelope, before SSE headers are sent:

MessageCause
Chain not foundfromChainId or toChainId has no Network record
From token not foundNo Token matching the address and chain
To token not foundSame for the destination

Side effect: Redis cache

For every quote, services/swap.service.js writes the full object under meta.id with a 300-second TTL:

js
redis.setEx(meta.id, 60 * 5, JSON.stringify({ data, meta, restProps }))

That is what GET /defi/swap/:id reads. Only meta reaches the browser.

Side effect: price enrichment

The service stamps priceUSD onto both tokens from the in-memory Binance cache before calling the SDK.

Why the enrichment exists

The SDK computes the Blazpay 0.1% display from amount × fromToken.priceUSD. The Token schema has no priceUSD column, so without enrichment the SDK falls back to quote.usdAmount — which OKX and Nordstern report as 0, producing a $0 platform-fee display. Stablecoins short-circuit to 1.00 inside getTokenPrice. It is best-effort; a failure degrades to the old behaviour.

Default excludes

Applied when the caller passes none:

ListMembers
excludeSwapsymbiosis, one_inch, open_ocean (+ icecream_swap on Polygon)
excludeBridgeicecream_swap, one_inch, butter_network, open_ocean

GET /defi/swap/:id — build the transaction

Rehydrates a cached quote and asks the provider to build calldata.

ParamNotes
:idThe meta.id from the SSE stream
Any query paramMerged into quotePayload as an override — this is how a changed slippage or recipient is applied without re-quoting

Response: { tx: { to, data, value }, spender, metaData? }

metaData is present for providers that broadcast themselves (the Kima pattern), where tx is null.

Fails after 5 minutes

The Redis key expires. A user who leaves the confirm screen open then clicks confirm gets an error rather than a stale price. Re-quoting on window focus is the correct product fix and is not implemented.

POST /defi/sign — EIP-712 signature

BodyNotes
metaTxThe full MetaTransaction struct
chainIdSelects the signer key and the verifying contract

Signer selection: POL_TX_SIGNER for chain 137, TX_SIGNER otherwise.

Domain:

js
{ name: 'BlazpayRelayer', version: '1', chainId, verifyingContract: relayerAddresses(chainId) }

Response: the signature, to be passed as the second argument to executeMetaTransactionSwap.

What the signature means

It attests that Blazpay built this route — the relayer requires ECDSA.recover(hash, sig) == signer. The user's authority is separate: they are msg.sender and pay gas. See custody.

GET /defi/relayer/tx/data — headless calldata

Returns { approvalData, executeData, value, to } for callers with no BrowserProvider.

This route is unreachable as registered

routes/defi.routes.js has router.get('relayer/tx/data', …)missing the leading slash. The intended /api/defi/relayer/tx/data does not resolve. The underlying function (swapController.getSignTxRawData, backed by TradeManager.sendSignTxDataRaw) works; only the route registration is broken.

GET /defi/init/:address — wallet balances

Controller controllers/defi/init.js → getWalletBalances. Bootstraps the trading UI with the wallet's balances.

Transaction history

MethodPathPurpose
GET/defi/transaction/:walletAddressList a wallet's trades
GET/defi/transaction/:walletAddress/:idOne record by id
GET/defi/tx/:hashBy hash
GET/defi/txHash/:txHashBy hash (alternate)
GET/defi/tx/wallet/:addressMost recent
GET/defi/txn/:walletAddressRefresh pending statuses — polls each provider's status endpoint
POST/defi/transactionCreate a record
PATCH/defi/transaction/hash/:hashUpdate by uuid or hash
PATCH/defi/transaction/updateBridge status update

Model: models/defiTransactions.js.

The UUID-first pattern

sendTransactionRaw creates the record with a UUID as txHash, pushes ?hash=<uuid> into the browser URL so a reload recovers the in-flight trade, then PATCH /defi/transaction/hash/<uuid> swaps in the real hash. Records whose txHash still looks like a UUID are trades that never got one. No cleanup job exists.

GET /defi/fees/summary — accumulated relayer fees

Walks contract-deployment/scripts/blazpayRelayer.deployments.json, reads native and known-token balances per chain over RPC, prices via CoinGecko, returns a USD total.

ParamEffect
force=1Bypass the 5-minute Redis cache (fees:summary:v1)

Only tokens with a coingeckoId in the Token collection are checked — long-tail tokens never accumulate as fees, and skipping them keeps the RPC fan-out inside the timeout on big-catalogue chains. Per-chain RPC calls have an 8-second timeout.

Two-round-trip flow

1. GET  /defi/quotes?…                 → SSE meta objects (cached 5 min)
2. user picks
3. GET  /defi/swap/:id                 → { tx, spender, metaData? }
4. POST /defi/sign  { metaTx, chainId} → signature
5. wallet sends executeMetaTransactionSwap(metaTx, signature)
6. POST /defi/transaction              → record with a UUID txHash
7. PATCH /defi/transaction/hash/:uuid  → real hash
8. GET  /defi/txn/:wallet              → status refresh

Full walkthrough: quote → settle pipeline.

Other trading routes

PathPurpose
POST /defi/order/create, GET /defi/order/:walletAddressOrder records
POST /defi/orderNewer buyController.createOrder
POST /defi/on-off-ramp-quotesUnified fiat comparison — see fiat API
POST /defi/bridge/qubeQube bridge completion
POST /defi/save-sca, GET /defi/get-sca/:addressSmart-contract-account records
POST /defi/witwit.ai NLU (legacy)
GET /defi/transak/assignReference assignment

Failure modes

SymptomCause
Fewer routes than expectedProvider APIs failing — errors are swallowed per aggregator by design
Chain not foundNo Network record, or show: false
From/To token not foundNo Token record for that (address, chainId)
Build returns an errorThe 5-minute Redis key expired
Platform fee displays as $0Price enrichment failed and the provider reports usdAmount: 0 (OKX, Nordstern)
relayerAddresses throwsNo relayer on that chain — Ethereum and Etherlink by design

More: errors & failure modes.

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.