Appearance
API conventions
Read this before the endpoint pages. The API predates any style guide, so the conventions are descriptive rather than prescriptive — but knowing them saves a lot of guessing.
Base URLs
| Environment | Base |
|---|---|
| Production | https://api.blazpay.com/api |
| Local | http://localhost:5000/api |
The frontend resolves it in defi-dex/src/utils/axios.ts:
ts
baseURL = VITE_API_URL || (isLocalhost ? 'http://localhost:5000/api'
: 'https://api.blazpay.com/api')swap-sdk resolves its own (swap-sdk/src/utils/constants.ts), defaulting to https://api.blazpay.com/api/defi and only using localhost when the browser host matches localhost, 127. or 192.168.. In Node it always uses production. Override with configure({ baseApiUrl }).
Response envelope
Most /api endpoints use sendResponse from utils/helpers.js:
json
{
"status": 200,
"message": "success",
"data": { },
"errors": null,
"notShowPopUp": true
}| Field | Notes |
|---|---|
status | Duplicates the HTTP status |
message | Defaults to "success" |
data | The payload, null on error |
errors | null on success; an object or string otherwise |
notShowPopUp | A UI hint, not an API concept — tells the frontend whether to surface a toast. Defaults to true |
The envelope is not universal
Several controllers — particularly the provider proxies and the newer /api/v1 surface — return the upstream payload directly, or a bare object. Do not write a client that assumes .data unwrapping everywhere. Check the endpoint page.
There is also sendResponseAi(res, statusCode, message, data, save, errors) for the legacy AI chat path, with a save flag instead of notShowPopUp.
Authentication
Four different patterns coexist. Which one applies depends on the route, not on a header convention.
| Pattern | Where | Mechanism |
|---|---|---|
| None — wallet in the body/params is trusted | Most /defi/*, all BlazAI | The wallet value is taken at face value |
| JWT bearer | /user, /admin, rewards, games | middleware/authMiddleware.js, JWT_SECRET / JWT_ADMIN_SECRET |
| Wallet signature | Mutating Autopilot routes | middleware/walletAuth.js — see below |
| Socket auth | Socket.io namespaces | middleware/authSocketMiddleware.js |
The wallet-signature pattern
body = { ...payload, wallet, timestamp, signature }
signedBody = body minus `signature`
digest = sha256(utf8(JSON.stringify(sortDeep(signedBody)))) // keys sorted recursively
message = `Blazpay <action> ${digest} ${timestamp}`
signature = personal_sign(message) // EIP-191| Property | Value |
|---|---|
| Window | ±5 minutes |
| Replay guard | Redis, TTL 600 s |
| Scope | The action string is inside the message, so a signed pause cannot replay as create |
| Result | req.authWallet, lowercased and verified |
Handlers must read req.authWallet, never req.body.wallet.
Why most routes are unauthenticated
For DCA, funds only move via user-signed transactions — the backend merely records status, so a forged request cannot steal anything. For Autopilot, a forged "raise my spend cap" would change what the executor does with escrowed funds, so it needs the signature. The asymmetry is deliberate, not an oversight.
That said: audit any new route that touches escrowed value. See security model.
Server-Sent Events
Two SSE endpoints, with different framing.
Quotes — GET /defi/quotes
data: {"id":"…","aggregator":"lifi","amount":"…", …}\n\n
data: {"id":"…","aggregator":"odos", …}\n\n
event: end\n\nEach data: line is one provider's meta object. Clients append and re-sort; the best route changes as quotes arrive. An end event closes the stream.
Consume with EventSource, not fetch — the frontend uses new EventSource(baseURL + '/defi/quotes?...').
Chat — POST /v1/blaz/message/stream
Three named event types:
| Event | Payload | Order |
|---|---|---|
meta | { action, data } | First, so the card can render before prose |
token | A text chunk | Repeated |
done | — | Last |
Client: defi-dex/src/apis/ai.api.ts → sendAIMessageStream({ onToken, onMeta, onDone }).
Addresses
| Rule | Detail |
|---|---|
| Casing | Accepted in any casing. Lowercased on write in Autopilot policies and wallet auth, because hashes depend on it |
| Native asset | Three sentinels mean native: 0x0000…0000, 0xeeee…eeee, and Polygon's 0x…1010 |
| TRON | Base58 (T…) externally, '41' + hex internally. Convert with tronweb.address.fromHex |
| Comparison | Always compare lowercased. Token and Network lookups do |
Chain identifiers
chainId in a request means Network.id, which is the EVM chain id for EVM chains and a synthetic value otherwise:
| Chain | chainId |
|---|---|
| Any EVM chain | Its real chain id |
| Solana | 102 |
| TRON | 728126428 |
102 is not a real chain id
Anything treating chainId as EVM-shaped — a relayer lookup, an explorer URL builder — must special-case the non-EVM two.
Amounts
| Context | Representation |
|---|---|
| Quote requests | Human units (1.5 for 1.5 ETH) |
Quote meta.amount | Base units as a string |
Autopilot policy amount | A positive numeric string — /^\d+(\.\d+)?$/ — never a JS number |
| Contract calls | Base units, BigNumber/bigint |
Why policy amounts are strings
Float precision drift between the frontend's hash input and the backend's would change the keccak256, so the policy signature would not verify. Strings avoid it entirely.
Idempotency and correlation
There is no Idempotency-Key header. Correlation happens two ways:
- Quote id — a UUID from
meta.id, valid for 5 minutes in Redis. It correlates a quote to its build. - Transaction UUID — trade records are created with a UUID as
txHash, then patched with the real hash.?hash=<uuid>in the browser URL recovers an in-flight trade.
Pagination
Inconsistent. Some endpoints take page/limit, some take offset, some return everything. The approval scanner caps at 25 by allowance size; the token picker slices the top 200 by order. Check the endpoint.
Caching
| Cache | Key | TTL |
|---|---|---|
| Quotes | meta.id in Redis | 5 min |
| Fee summary | fees:summary:v1 | 5 min, bypass with ?force=1 |
| Token safety scans | (address, chainId) in Mongo | 6 h |
| Wallet cards | Mongo | Until POST /generate |
| Provider catalogues | Mongo | Until the save-* endpoint is called |
| Autopilot prices | Redis rolling series | Rolling window |
HTTP semantics
Loose. GET is used for some administrative mutations (GET /defi/alchemy/save-crypto refreshes a cache), POST for some reads (POST /defi/1inch is a proxied query). PATCH appears on transaction updates. Do not infer safety from the verb.
Body limit
express.json({ limit: '20mb' }) — large enough for pasted Solidity and generated dApp HTML.
CORS
An explicit allowlist in bz-backend/index.js with credentials: true. Socket.io CORS is configured separately in services/socket.js. Full list: domains.
Rate limiting
express-rate-limit is a dependency. Verify where it is actually applied before assuming a route is protected — most are not.
Known route bugs
| Bug | Detail |
|---|---|
router.get('relayer/tx/data', …) | Missing leading slash in routes/defi.routes.js, making the intended /api/defi/relayer/tx/data unreachable |
/api/supportForDasboard | Typo in a live path. Fixing it is a breaking change |
/api/clippers and /api/v1/clippers | The same route file mounted twice |
/api/nft | Mounted twice — nftRoutes then nftMetadataRoutes |