Wallet-authenticated API · v1
Ignite Authenticated Trading API
Connect an EVM wallet, create a short-lived Ignite session, read the wallet's account state, and place or cancel orders. Standard wallets use EIP-712; Thanos Wallet uses its published SIWE personal_sign flow. Authentication proves wallet control; Ignite never requests a private key or seed phrase.
https://api.ignite.tradeAuthentication flow
Send the wallet address. The challenge is valid for five minutes and replaces any earlier challenge for that address.
Sign the returned typedData object as EIP-712 data. Do not sign the human-readable challenge hint.
Send the address and signature. A valid, unused challenge returns an eight-hour bearer session.
Send Authorization: Bearer <token>. Responses are always scoped to the authenticated wallet.
Request a wallet challenge
POST /v1/auth/challengeThe EIP-712 domain intentionally has no chain ID, so the login is not tied to the wallet's currently selected network.
curl -X POST https://api.ignite.trade/v1/auth/challenge \
-H "Content-Type: application/json" \
-d '{"address":"0xYOUR_WALLET"}'
{
"typedData": {
"domain": { "name": "Ignite DEX", "version": "1" },
"types": { "Login": [
{ "name": "address", "type": "address" },
{ "name": "nonce", "type": "string" },
{ "name": "issuedAt", "type": "string" }
]},
"primaryType": "Login",
"message": {
"address": "0xYOUR_WALLET",
"nonce": "single-use-random-value",
"issuedAt": "2026-07-27T12:00:00.000Z"
}
},
"expiresIn": "5m"
}Verify and create a session
POST /v1/auth/verify// viem example
const challenge = await fetch('https://api.ignite.trade/v1/auth/challenge', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ address: account.address })
}).then((response) => response.json());
const signature = await walletClient.signTypedData({
account,
...challenge.typedData
});
const session = await fetch('https://api.ignite.trade/v1/auth/verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ address: account.address, signature })
}).then((response) => response.json());
// { "verified": true, "token": "<session JWT>" }Thanos Wallet SIWE compatibility
GET + POST /api/auth/nonce · /api/auth/verifyThanos Wallet signs an EIP-4361 SIWE message with personal_sign. Request a nonce for the wallet, build the SIWE message with the current Ignite domain and URI, sign that exact message, then verify it. The returned sessionToken is the same eight-hour bearer credential used by all protected routes.
const nonce = await fetch(
'https://api.ignite.trade/api/auth/nonce?address=' + account.address
).then((response) => response.text());
// Use thanos-connect's buildSiweMessage() so the signed bytes match.
const message = buildSiweMessage({
domain: window.location.host,
address: account.address,
uri: window.location.origin,
statement: 'Sign in to Ignite DEX with your Thanos Wallet.',
chainId,
nonce,
expirationTime: new Date(Date.now() + 5 * 60_000).toISOString()
});
const signature = await walletClient.signMessage({ account, message });
const { sessionToken } = await fetch('https://api.ignite.trade/api/auth/verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ address: account.address, message, signature })
}).then((response) => response.json());The nonce endpoint answers text/plain (32 hex characters). It is valid for five minutes and one login; requesting another nonce for the same address replaces it. The server checks the following and nothing else: the personal_sign signature recovers to address; line 1 names an Ignite web host as the domain (ignite.trade or www.ignite.trade); line 2 is the same address; the Nonce: line carries the current nonce; and Expiration Time:, if present, is in the future. URI, Version, Chain ID, Statement and Issued At are not checked. Lines are separated by a single LF, with no trailing newline.
ignite.trade wants you to sign in with your Ethereum account:
0x1111111111111111111111111111111111111111
Sign in to Ignite DEX with your Thanos Wallet.
URI: https://ignite.trade
Version: 1
Chain ID: 1
Nonce: 3f6c1d0e9b8a7c6d5e4f30211a2b3c4d
Issued At: 2026-10-10T12:00:00.000Z
Expiration Time: 2026-10-10T12:05:00.000Z| 401 code | Meaning |
|---|---|
| signature_mismatch | The signature does not recover to address over the exact message bytes. |
| malformed_message | Not an EIP-4361 message, or an unreadable Expiration Time. |
| domain_mismatch | Line 1 names a domain other than an Ignite web host. |
| address_mismatch | Line 2 is not the address in the request body. |
| message_expired | Expiration Time has passed. |
| nonce_missing | The message has no Nonce line. |
| nonce_invalid | Not the current nonce for this address: never issued, replaced by a newer one, or already used. |
| nonce_expired | The nonce is more than five minutes old. |
/v1/auth/verify returns it as token and /api/auth/verify as sessionToken, because the latter follows the Thanos Connect SDK contract.Authoritative market rules
Read GET /v1/markets before submitting an order. Each market record is authoritative for symbol, type, status, tradingEnabled, feeBps, tickSize, minOrderSize, maxOrderSize, token symbols, and network ID.
Limit prices must be a multiple of tickSize; sizes (base units) a multiple of sizeStep and within minOrderSize–maxOrderSize. A null field is not enforced. A quoteAmount market buy is rounded down to whole size steps.
Takers pay takerFeeBps (equal to feeBps) of notional in the quote asset; makers pay nothing (makerFeeBps is 0). Each fill reports the fee you paid.
Balances and positions
GET /v1/portfolio/{address}Returns equity, free collateral, balances and positions for the authenticated wallet only. The path address must match the bearer-token subject; another wallet is rejected. reserved is held by your resting limit orders (bids hold quote, asks hold base); available is what new orders, liquidity deposits and withdrawals can use. Withdrawals are deducted from total when requested. amount equals total and is kept for compatibility.
curl https://api.ignite.trade/v1/portfolio/0xYOUR_WALLET \
-H "Authorization: Bearer $IGNITE_TOKEN"
{
"address": "0xyour_wallet",
"equity": 1520.4,
"freeCollateral": 1520.4,
"balances": [
{ "asset": "USDC", "amount": 1200, "total": 1200, "reserved": 220.86, "available": 979.14 },
{ "asset": "SOL", "amount": 2.9, "total": 2.9, "reserved": 0, "available": 2.9 }
],
"positions": []
}Place an order
POST /v1/orderscurl -X POST https://api.ignite.trade/v1/orders \
-H "Authorization: Bearer $IGNITE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"market": "LITHO/USDT",
"side": "buy",
"type": "limit",
"price": 8.70,
"size": 1,
"clientOrderId": "strategy-a-000001",
"timeInForce": "GTC",
"postOnly": false,
"reduceOnly": false
}'| Field | Accepted values | Rule |
|---|---|---|
| market | Published market symbol | Required; must be active and trading-enabled. |
| side | buy | sell | Required. Market-specific restrictions still apply. |
| type | limit | market | Required. Limit orders should include price. |
| price | Positive number | Required for limit orders. Must be a multiple of tickSize when the market publishes one. |
| size | Positive number | Base units. Required except quote-denominated market buys; sizeStep and min/max rules apply. |
| quoteAmount | Positive number | Only for market buys; cannot be combined with size. |
| clientOrderId | 1–128 characters | Idempotency key, unique per wallet and remembered permanently. Reusing one returns the original order and its fills; the rest of the new request is ignored. Without it, every request is a new order. |
| timeInForce | GTC | IOC | FOK | Defaults to GTC. Market orders terminate any unfilled remainder. |
| postOnly | boolean | Rejects an order that would immediately take liquidity. |
| reduceOnly | boolean | Relevant only to supported position markets; cannot increase or flip a position. |
| leverage | 1–50 | Accepted and stored but ignored on spot markets: spot orders are always fully funded from your balance. |
| account | Wallet address | Optional; if supplied it must equal the authenticated wallet. |
An order the matching engine refuses comes back as HTTP 200 with accepted: false, status: "rejected" and a rejectCode with rejectReason: POST_ONLY_WOULD_CROSS or FOK_NOT_FILLED. They are sent on the placing response only; a clientOrderId replay returns the order without them.
List wallet orders
GET /v1/ordersReturns the wallet's latest 100 orders, newest first, in open and terminal states. Each order carries createdAt, updatedAt (last status or fill change), remainingSize, and every fill it took part in as taker or maker. The place-order response returns the same object as order, alongside createdAt and updatedAt.
curl https://api.ignite.trade/v1/orders \
-H "Authorization: Bearer $IGNITE_TOKEN"
{ "orders": [{
"id": "cmg…", "market": "SOL/USDC", "side": "sell", "type": "limit", "timeInForce": "GTC",
"postOnly": true, "price": 110.5, "size": 2, "filledSize": 0.5, "remainingSize": 1.5,
"status": "partially_filled", "clientOrderId": "strategy-a-000002",
"createdAt": "2026-10-10T12:00:00.000Z", "updatedAt": "2026-10-10T12:03:10.000Z",
"fills": [{ "id": "…", "orderId": "cmg…", "role": "maker", "price": 110.5, "size": 0.5,
"notional": 55.25, "feeAmount": 0, "feeAsset": "USDC", "createdAt": "2026-10-10T12:03:10.000Z" }]
}] }Cancel an order
DELETE /v1/orders/{orderId}Only the authenticated owner can cancel a live order. Filled, rejected, expired, failed, or already-cancelled orders cannot be cancelled (ORDER_NOT_FOUND).
curl -X DELETE https://api.ignite.trade/v1/orders/ORDER_ID \
-H "Authorization: Bearer $IGNITE_TOKEN"Cancel all open orders
DELETE /v1/orders?market={symbol}Cancels every open, resting and partially filled order the wallet has when the request runs, each exactly as the single cancel would; a partially filled order ends partially_filled_cancelled. The optional market filter takes the published symbol, URL-encoded. It works while trading is paused, so it can be used as an emergency stop. Orders placed after the call starts are not included; an unknown market is MARKET_NOT_FOUND.
curl -X DELETE "https://api.ignite.trade/v1/orders?market=SOL%2FUSDC" \
-H "Authorization: Bearer $IGNITE_TOKEN"
{
"cancelled": [
{ "id": "cmh…", "clientOrderId": "strategy-a-000002", "market": "SOL/USDC", "status": "cancelled" },
{ "id": "cmh…", "clientOrderId": null, "market": "SOL/USDC", "status": "partially_filled_cancelled" }
],
"count": 2
}Wallet fills
GET /v1/trades?market={symbol}&limit=100Returns fills where the authenticated wallet was maker or taker, newest first. The optional limit is clamped to 1–500 and the optional market filter uses the published market symbol. Each fill has orderId (your order), side, role, notional, and the fee you paid as feeAmount in feeAsset (0 as maker).
{ "trades": [{
"id": "…", "orderId": "cmh…", "market": "SOL/USDC", "side": "buy", "role": "taker",
"price": 110.5, "size": 0.5, "notional": 55.25, "feeAmount": 0.05525, "feeAsset": "USDC",
"counterparty": "0x…", "createdAt": "2026-10-10T12:03:10.000Z"
}] }Responses and integration safety
Malformed input, invalid signature, market rule failure, insufficient balance, or an order that cannot be accepted.
Missing, invalid, or expired bearer token. Request a new challenge and session; do not retry with the same expired token.
The requested account or object does not belong to the authenticated wallet.
Unknown market or resource. Refresh the market list before retrying.
Account state requires a current action, such as accepting a new terms version.
Honor RateLimit headers and retry only after the published reset time with exponential backoff.
Trading is paused (code OPERATIONAL_PAUSED). Retry later; nothing was placed.
Every error carries a stable code next to the message: { "error": string, "code": string }. Validation failures put the field details in error; unknown routes and unexpected errors use { "statusCode", "code", "error", "message" }. Branch on code, not on the message text, which may be reworded. Codes are never renamed or reused; new ones may be added. Route-specific codes keep their values: STALE_TERMS_VERSION (409), the lowercase Thanos login codes above, and Fastify FST_ERR_* codes for malformed HTTP requests such as invalid JSON.
| code | HTTP | Meaning |
|---|---|---|
| VALIDATION_ERROR | 400 | A required field is missing or a value is invalid; error holds the field details. |
| INVALID_ORDER | 400 | Non-positive size, a limit order without a price, post-only on a market or non-GTC order, or leverage above 100. |
| MARKET_NOT_FOUND | 400 | No market has this symbol (also cancel-all with an unknown market). |
| MARKET_NOT_TRADING | 400 | The market exists but is not accepting orders. |
| MARKET_PRE_OPEN | 400 | The pair has not opened: only resting limit orders that do not cross are accepted. |
| NO_REFERENCE_PRICE | 400 | The market has no valid mark price to value the order. |
| ORDER_BELOW_MIN_SIZE | 400 | Size is below minOrderSize. |
| ORDER_ABOVE_MAX_SIZE | 400 | Size is above maxOrderSize. |
| SIZE_STEP | 400 | Size is not a multiple of sizeStep. |
| PRICE_TICK | 400 | Price is not a multiple of tickSize. |
| SELL_DISABLED | 400 | Selling is disabled on this pair (buy-only launch pairs). |
| INSUFFICIENT_BALANCE | 400 | The available balance, after what your open orders reserve, does not cover the order and its fee. |
| QUOTE_AMOUNT_TOO_SMALL | 400 | A market buy by quoteAmount buys less than one size step. |
| NO_LIQUIDITY | 400 | A market buy by quoteAmount found no ask liquidity. |
| REDUCE_ONLY | 400 | A reduce-only order would open, increase or flip a position. |
| RISK_LIMIT | 400 | The order would breach the account margin requirements (position markets). |
| SETTLEMENT_FAILED | 400 | The order matched but could not be settled; nothing moved and the order is recorded as rejected. |
| CUSTODY_HOLD | 400 | The account is on a custody hold pending reconciliation. |
| ORDER_NOT_FOUND | 400 | Cancel: the order does not exist, belongs to another wallet, or is already closed. |
| BAD_REQUEST | 400 | Refused for a reason without a more specific code. |
| UNAUTHORIZED | 401 | Missing, invalid or expired bearer token. |
| FORBIDDEN | 403 | The token cannot do this, for example another wallet's account or an unlinked Google session. |
| NOT_FOUND | 404 | No such route or resource. |
| CONFLICT | 409 | The request conflicts with the current state. |
| RATE_LIMITED | 429 | Too many requests; wait for Retry-After. |
| RESTRICTED_JURISDICTION | 451 | Ignite is not available in your jurisdiction. |
| INTERNAL_ERROR | 500 | Unexpected server error. |
| OPERATIONAL_PAUSED | 503 | Trading is paused by the emergency kill switch; nothing was placed. |
| SERVICE_UNAVAILABLE | 503 | A dependency is temporarily unavailable. |
Rate limits
| Routes | Limit |
|---|---|
| /v1/auth/challenge and /api/auth/nonce | 30 per minute |
| /v1/auth/verify and /api/auth/verify | 30 per minute |
| /v1/market-data/* | 300 per minute |
| Orders, portfolio, trades, markets | No per-client limit at present; keep to about 10 requests per second per session. Limits added later will be announced and sent in the same headers. |
Limits count requests per bearer token (per IP without one) over a sliding 60-second window. Limited routes send RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. RateLimit-Reset is always an absolute Unix time in seconds: when the oldest request in the window expires. A 429 also sends Retry-After in seconds.
Public WebSocket streams
No authentication. Both streams push full snapshots every two seconds (not diffs); REST remains the reference.
wss://api.ignite.trade/wsFirst a {"type":"snapshot","markets":[…]} message with the GET /v1/markets records, then every two seconds one {"type":"ticker","symbol","mark","index","volume24h","openInterest","change24h"} message per market.
wss://api.ignite.trade/ws/market?symbol=SOL%2FUSDC&tf=1mEvery two seconds: {"type":"market","symbol","tf","ticker","book":{"bids","asks"},"trades","candle"}. book has up to 250 {price,size} levels per side; trades holds the latest 60 {price,size,side,time}; tf is 1m, 5m, 15m, 1h, 4h, 1D, 1W or 1M.
Private order and fill streams are not available; poll GET /v1/orders and GET /v1/trades.
Admin, custody-operator, RPC credentials, internal service routes, and automation-signer details are intentionally outside this public integration contract.
