Appearance
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
| Param | Required | Notes |
|---|---|---|
fromChainId | yes | Network.id |
toChainId | yes | Equal to fromChainId for a swap |
fromToken | yes | Address; native sentinels resolved |
toToken | yes | Address |
amount | yes | Human units (1.5, not wei) |
type | yes | swap → SWAP, anything else → BRIDGE |
srcWalletAddress | — | Quote context |
dstWalletAddress | — | Where output must land |
slippage | — | Default 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\nStream ends when all aggregators settle. Consume with EventSource.
Errors
404 with the standard envelope, before SSE headers are sent:
| Message | Cause |
|---|---|
Chain not found | fromChainId or toChainId has no Network record |
From token not found | No Token matching the address and chain |
To token not found | Same 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:
| List | Members |
|---|---|
excludeSwap | symbiosis, one_inch, open_ocean (+ icecream_swap on Polygon) |
excludeBridge | icecream_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.
| Param | Notes |
|---|---|
:id | The meta.id from the SSE stream |
| Any query param | Merged 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
| Body | Notes |
|---|---|
metaTx | The full MetaTransaction struct |
chainId | Selects 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
| Method | Path | Purpose |
|---|---|---|
GET | /defi/transaction/:walletAddress | List a wallet's trades |
GET | /defi/transaction/:walletAddress/:id | One record by id |
GET | /defi/tx/:hash | By hash |
GET | /defi/txHash/:txHash | By hash (alternate) |
GET | /defi/tx/wallet/:address | Most recent |
GET | /defi/txn/:walletAddress | Refresh pending statuses — polls each provider's status endpoint |
POST | /defi/transaction | Create a record |
PATCH | /defi/transaction/hash/:hash | Update by uuid or hash |
PATCH | /defi/transaction/update | Bridge 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.
| Param | Effect |
|---|---|
force=1 | Bypass 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 refreshFull walkthrough: quote → settle pipeline.
Other trading routes
| Path | Purpose |
|---|---|
POST /defi/order/create, GET /defi/order/:walletAddress | Order records |
POST /defi/order | Newer buyController.createOrder |
POST /defi/on-off-ramp-quotes | Unified fiat comparison — see fiat API |
POST /defi/bridge/qube | Qube bridge completion |
POST /defi/save-sca, GET /defi/get-sca/:address | Smart-contract-account records |
POST /defi/wit | wit.ai NLU (legacy) |
GET /defi/transak/assign | Reference assignment |
Failure modes
| Symptom | Cause |
|---|---|
| Fewer routes than expected | Provider APIs failing — errors are swallowed per aggregator by design |
Chain not found | No Network record, or show: false |
From/To token not found | No Token record for that (address, chainId) |
| Build returns an error | The 5-minute Redis key expired |
Platform fee displays as $0 | Price enrichment failed and the provider reports usdAmount: 0 (OKX, Nordstern) |
relayerAddresses throws | No relayer on that chain — Ethereum and Etherlink by design |
More: errors & failure modes.