0x9A03E9D5b38b9C2C87F69c654f6Ae1ACF11FAbA3
Non-custodial Solana swap & market-cap limit-order engine for AI agents. Multi-DEX (Raydium, PumpSwap, Orca, Meteora). Jito MEV-protected. Ed25519-verified server messages. No API key — keys never leave your client.
npx -y @traderouter/trade-router-mcp — Claude Desktop, Cursor, Cline, ElizaOS, LangChain, any MCP clientuv add traderouter-mcp then run traderouter-mcp — Python equivalent of the npm packageai.traderouter/trade-router-mcp@1.0.10 (DNS-auth published, isLatest).mcpregistry_* or .env file leaks into npm pack)main — no force-push, no deletion, required CI checksdist.signatures on every release)| Version | Area | Change |
|---|---|---|
| v1.4.0 | MCP | MCP server canonical migrated to @traderouter/trade-router-mcp@1.0.10 on npm and traderouter-mcp@1.0.10 on PyPI. MCP Registry namespace migrated to ai.traderouter/trade-router-mcp@1.0.10 (DNS-auth published, no device codes); io.github.TradeRouter/trade-router-mcp@1.0.9 retained as secondary listing. |
| v1.4.0 | MCP | New env var TRADEROUTER_DRY_RUN=true short-circuits every write-action tool (submit_signed_swap, auto_swap, place_*_order, cancel_order, extend_order) to { dry_run: true, tool, args } — agent thinks it executed, nothing actually moves on-chain. Read-only tools always execute. Useful for first-run agent testing. |
| v1.4.0 | Tests/CI | 10 regression tests pinning the params_hash preimage shape per order type (8 fields for limit/trailing, 10 for TWAP variants, 11 for limit_trailing_twap). Wired into GitHub Actions CI on every push across Node 18/20/22, plus a tarball-leak guard that fails the build if any .mcpregistry_* or .env file leaks into npm pack. |
| v1.4.0 | Docs | Site adds openapi.yaml alongside openapi.json, ships SECURITY.md threat model with explicit transparency about what the MCP server does NOT validate. New cookbook repo at github.com/TradeRouter/cookbook with 7 example agents (5 strategies + ElizaOS + LangChain integration). Glama listing live at glama.ai/mcp/servers/TradeRouter/trade-router-mcp. VirusTotal scan v1.0.10: 0/62 detections on both tarball and inner mjs. |
| v1.3.0 | MCP | MCP npm package scope changed from @re-bruce-wayne/trade-router-mcp to @traderouter/trade-router-mcp. Use npx @traderouter/trade-router-mcp and update Claude Desktop / Cursor configs (args: ["-y","@traderouter/trade-router-mcp"]). Old package name is deprecated. |
| v1.3.0 | WebSocket | Quantity removed for TWAP (and TWAP combo) orders; use amount or holdings_percentage (sell) instead. |
| v1.3.0 | WebSocket | Combo orders documented and wired: limit_twap_sell / limit_twap_buy (limit then TWAP), trailing_twap_sell / trailing_twap_buy (trail then TWAP), limit_trailing_sell / limit_trailing_buy (limit then trailing → single swap), limit_trailing_twap_sell / limit_trailing_twap_buy (limit → trail → TWAP). Server sends limit_trailing_activated when trailing phase starts; hybrid triggers use trailing_twap_triggered, limit_twap_triggered, limit_trailing_twap_triggered then twap_execution per slice. |
| v1.3.0 | Docs | Site, llms.txt, openapi.json, and SKILL.md updated with all combo order types, required/optional fields, and server message types (order_created / order_filled order_type enums; new request schemas in OpenAPI). |
| v1.2.0 | WebSocket | TWAP orders: new actions twap_buy and twap_sell. Split total amount into frequency slices over duration seconds. Use amount (or holdings_percentage for sell, resolved once at creation). Server sends twap_order_created, twap_execution per slice (with data.swap_tx and server_signature — verify before sign/submit), and twap_order_completed. Cancel returns twap_order_cancelled. |
| v1.2.0 | REST | GET /mcap?tokens=MINT1,MINT2 — market cap (and price/pool) data for token addresses. Returns object keyed by token address. |
| v1.2.0 | REST | GET /flex?wallet_address=…&token_address=… — flex trade card PNG for a wallet and token mint. Response image/png. |
| v1.2.0 | MCP | New tools: get_mcap (market cap for tokens), get_flex_card (flex card URL), place_twap_order (TWAP buy/sell). MCP verifies twap_execution.server_signature and auto-submits slices when private key is set. |
| v1.1.0 | Security | Clarified trust model: server public key is a hardcoded trust anchor (EXX3nRzfDUvbjZSmxFzHDdiSYeGVP1EGr77iziFZ4Jd4), not a runtime-fetched endpoint value. Fetching the key from the server at runtime is a TOCTOU vulnerability — do not do this. |
| v1.1.0 | Auth | Connection sequence changed: server now sends {"type":"challenge","nonce":"…"} as the first message (not subscribed). register must include a signature field — base58 Ed25519 signature of the nonce. Sending register without a signature returns authenticated: false and order actions are rejected. |
| v1.1.0 | Auth | registered message now includes authenticated: true|false. Clients must check this field — a registered response with authenticated: false means registration failed silently. Orders will never be received. |
| v1.1.0 | Fills | order_filled now carries server_signature (Ed25519 over canonical payload digest). Clients must verify this signature against the trust anchor before signing or submitting data.swap_tx. Unverified fills must be rejected. |
| v1.1.0 | Fills | already_dispatched: true fills must be skipped entirely — do not sign or submit. data and data.swap_tx may be absent when already_dispatched is true. |
| v1.1.0 | Orders | order_created now includes params_hash, server_signature, target_bps, trail_bps, slippage, expiry_hours, amount, holdings_percentage. When params_hash and server_signature are present, clients can verify the server's commitment to order parameters. |
| v1.1.0 | Fills | Staleness check (triggered_mcap / filled_mcap < 0.85) must be applied to every fill, not only after reconnect. Divide-by-zero guard: if filled_mcap is 0 or null, skip the ratio — the fill is valid. |
| v1.1.0 | Docs | Added dedicated Auth & Registration, Handling Fills, and Troubleshooting sections. Updated all examples to use challenge flow with nacl signing. Removed all subscribed-based registration examples. |
| v1.1.0 | Docs | Added key rotation instructions: TRADEROUTER_SERVER_PUBKEY_NEXT. If primary key fails verification, try next key; on success, server has rotated — update primary key. Rotation announcements at api.traderouter.ai/security. |
A Model Context Protocol server for TradeRouter.ai — Solana swap & limit order engine. Use it from Claude Desktop, Cursor, or any MCP host.
# npm (Node.js) npx -y @traderouter/trade-router-mcp # PyPI (Python equivalent, runs the same 21 tools) uv add traderouter-mcp traderouter-mcp
Add the following to your claude_desktop_config.json. Set TRADEROUTER_DRY_RUN=true on first run — every write tool returns { dry_run: true, ... } instead of touching mainnet, so you can verify everything end-to-end before allocating funds.
{
"mcpServers": {
"traderouter": {
"command": "npx",
"args": ["-y", "@traderouter/trade-router-mcp"],
"env": {
"TRADEROUTER_PRIVATE_KEY": "your_base58_private_key",
"TRADEROUTER_DRY_RUN": "true"
}
}
}
}
Same shape works in Cursor (~/.cursor/mcp_servers.json) and Cline.
Both ElizaOS and LangChain ship official generic MCP-client adapters — no Trade Router-specific code needed. See github.com/TradeRouter/cookbook:
@fleek-platform/eliza-plugin-mcp. Drop a mcp.servers.traderouter block in your character JSON; ElizaOS auto-discovers all 21 Trade Router tools.langchain-mcp-adapters (Python) or @langchain/mcp-adapters (TypeScript). 6 lines of code, full MultiServerMCPClient example.| OS | Config path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
| Variable | Required | Description |
|---|---|---|
TRADEROUTER_PRIVATE_KEY | ✓ | Solana wallet private key in base58 format. Read once at MCP server start, used for local signing, never transmitted. See SECURITY.md. |
TRADEROUTER_DRY_RUN | — | Default false. When true, every write-action tool (submit_signed_swap, auto_swap, place_*_order, cancel_order, extend_order) short-circuits and returns { dry_run: true, tool, args, note } instead of calling the API. Read-only tools execute normally. Added in 1.0.9. |
SOLANA_RPC_URL | — | RPC endpoint. Defaults to https://api.mainnet-beta.solana.com |
TRADEROUTER_SERVER_PUBKEY | — | Server public key for signature verification |
TRADEROUTER_SERVER_PUBKEY_NEXT | — | Next server public key for key rotation |
TRADEROUTER_REQUIRE_SERVER_SIGNATURE | — | Verify server signatures on fills. Defaults to true |
TRADEROUTER_REQUIRE_ORDER_CREATED_SIGNATURE | — | Verify server signatures on order creation. Defaults to true |
| Tool | Description |
|---|---|
get_wallet_address | Get the configured wallet address |
build_swap | Build an unsigned swap transaction |
submit_signed_swap | Submit a manually signed transaction |
auto_swap | Build and auto-sign a swap in one step |
get_holdings | Get token holdings for a wallet |
get_mcap | Get market cap data for token address(es) |
get_flex_card | Get flex trade card PNG URL for wallet + token |
place_limit_order | Place a limit buy or sell order |
place_trailing_order | Place a trailing buy or sell order |
place_twap_order | Place a TWAP buy or sell order (time-weighted slices) |
list_orders | List all active orders for a wallet |
check_order | Check the status of an order |
cancel_order | Cancel an active order |
extend_order | Extend an order's expiry |
connect_websocket | Connect and register WebSocket for a wallet |
connection_status | Get current WebSocket connection status |
get_fill_log | Get log of filled orders |
Build an unsigned Solana swap transaction. Returns base58 serialized tx — client signs, then submits via POST /protect.
holdings_percentage (bps, 10000 = 100%). Buy uses amount (lamports). Never mix them. Sending both is invalid (422).
API default slippage is 500 but use 1500–2500 for low-liquidity or newly launched tokens — 500 bps will fail on most memecoins.
{
"wallet_address": "string — Solana wallet pubkey",
"token_address": "string — SPL token mint",
"action": "buy | sell",
"amount": integer — lamports (buy only),
"holdings_percentage": integer — bps (sell only, 10000 = 100%),
"slippage": integer — bps, default 500 (allowed 100–2500; use 1500–2500 for low-liq/new tokens)
}
{
"status": "success",
"data": {
"swap_tx": "base58 unsigned tx — sign and POST /protect",
"token_address": "string",
"pool_type": "string — open enum (raydium, pumpswap, orca, meteora, …)",
"pool_address": "string — AMM pool ID",
"amount_in": integer,
"min_amount_out": integer,
"price_impact": number,
"slippage": integer,
"decimals": integer
}
}
{ "status": "error", "error": "message", "code": 400 }
code is optional. Common: 422 (validation), 400 (bad request).
"Error running simulation" means the route is unsellable right now (dead/rugged pool, zero effective balance, no route). Put the token on cooldown and avoid tight retry loops.
Submit a signed transaction (base64) through the MEV-protected priority lane. Blocks until confirmed on-chain, then returns signature and balance changes so you can update holdings state.
/swap returns swap_tx as base58. /protect expects base64. You must decode base58 → deserialize → sign → serialize → encode base64. Never send base58 directly to /protect.
/protect. If timeout fires, check tx status via RPC before retrying — the tx may have already landed. On 503, fallback to direct RPC is possible (no MEV protection).
{ "signed_tx_base64": "string — base64-encoded signed serialized transaction" }
{
"status": "success",
"signature": "string — transaction signature",
"sol_balance_pre": integer — lamports before tx,
"sol_balance_post": integer — lamports after tx,
"token_balances": [
{
"mint": "string — SPL token mint",
"balance": integer,
"decimals": integer,
"balance_change": integer,
"ui_amount_string": "string"
}
]
}
POST /swap → read data.swap_tx (base58)VersionedTransactionPOST /protect with { "signed_tx_base64": "…" }sol_balance_post + token_balances to update holdings stateScan Solana wallet token holdings with liquid DEX pool info.
{ "wallet_address": "string" }
Empty wallet: {}
{
"data": [
{
"address": "string — SPL token mint",
"valueNative": integer,
"amount": integer,
"decimals": integer
}
]
}
Keep a defensive valueNative > 0 filter before sells. The endpoint is intended to return sellable tokens but edge cases can appear.
Return market cap data for given token addresses.
tokens — comma-delimited list of Solana mint addresses.
GET https://api.traderouter.ai/mcap?tokens=MINT1,MINT2
Object keyed by token address. Empty object if no tokens provided or none found.
{
"<token_address>": {
"marketCap": number,
"pair_address": "string",
"pool_type": "string",
"priceUsd": number | null
}
}
Generate a flex trade card PNG for a wallet and token mint.
wallet_address — Solana wallet pubkey. token_address — SPL token mint.
GET https://api.traderouter.ai/flex?wallet_address=WALLET&token_address=MINT
Image PNG (Content-Type: image/png). 400 on invalid params; 501 if flex_card_image deps not available; 500 on server error.
Market-cap-based limit orders and trailing stops. Server checks mcap every ~5 seconds. When a target is crossed, it pushes order_filled with an unsigned swap tx for the client to sign and submit.
| Action | Direction | Use case | Key param |
|---|---|---|---|
sell | target > 10000 | Take-profit (mcap doubles = 20000) | holdings_percentage + target |
sell | target < 10000 | Stop-loss (mcap halves = 5000) | holdings_percentage + target |
buy | target < 10000 | Dip buy (mcap halves = 5000) | amount + target |
buy | target > 10000 | Breakout entry (mcap doubles = 20000) | amount + target |
trailing_sell | — | Auto-adjusting stop from peak | holdings_percentage + trail |
trailing_buy | — | Auto-adjusting entry from trough | amount + trail |
twap_sell | — | Time-weighted sell over duration | amount or holdings_percentage + frequency + duration |
twap_buy | — | Time-weighted buy over duration | amount + frequency + duration |
limit_twap_sell | — | Limit target then TWAP sell | target + frequency + duration |
limit_twap_buy | — | Limit target then TWAP buy | target + amount + frequency + duration |
trailing_twap_sell | — | Trail triggers then TWAP sell | trail + frequency + duration |
trailing_twap_buy | — | Trail triggers then TWAP buy | trail + amount + frequency + duration |
limit_trailing_sell | — | Limit then trailing stop → single swap | target + trail |
limit_trailing_buy | — | Limit then trailing stop → single swap | target + trail + amount |
limit_trailing_twap_sell | — | Limit then trail then TWAP sell | target + trail + frequency + duration |
limit_trailing_twap_buy | — | Limit then trail then TWAP buy | target + trail + amount + frequency + duration |
target is bps vs current mcap at order placement time, not your wallet entry price. It accepts any positive integer (no maximum). trail is bps callback distance from peak (sell) or trough (buy).
holdings_percentage resolves at trigger time, not placement — a pending 100% sell will sell 100% of the remaining balance if you've partially sold between placement and execution.
{"action": "check_order", "order_id": "uuid"}
{"action": "list_orders"}
{"action": "cancel_order", "order_id": "uuid"}
{"action": "extend_order", "order_id": "uuid", "expiry_hours": 336}
Order expiry is silent — no expiry event is sent. Poll list_orders or check_order to detect expired orders. Active orders persist server-side across reconnects.
DCA is not auto-chained. Each fill must be signed/submitted, then the next buy order must be placed explicitly by the client. The server does not chain orders automatically.
twap_buy and twap_sell split a total amount into frequency equal slices executed every duration / frequency seconds. duration is in seconds (min 60, max 30 days). For twap_sell you can use amount (raw token units) or holdings_percentage (bps, resolved once at order creation). For twap_buy use amount (SOL lamports). Server sends twap_execution for each slice (with data.swap_tx when successful) and twap_order_completed when all slices are done. Verify twap_execution.server_signature before signing/submitting each slice. Cancel via cancel_order; response type is twap_order_cancelled.
Limit + TWAP: limit_twap_sell / limit_twap_buy — wait for limit target, then execute via TWAP. Trailing + TWAP: trailing_twap_sell / trailing_twap_buy — when trail triggers, spawn TWAP. Limit + Trailing: limit_trailing_sell / limit_trailing_buy — wait for limit, then trailing stop; when trail triggers, single swap. Server sends limit_trailing_activated when the trailing phase starts. Limit + Trailing + TWAP: limit_trailing_twap_sell / limit_trailing_twap_buy — limit → trail → on trail trigger, spawn TWAP. Hybrid triggers use trailing_twap_triggered, limit_twap_triggered, or limit_trailing_twap_triggered (then twap_execution per slice).
| Action | Required fields | Optional |
|---|---|---|
register | wallet_address, signature (base58 of nonce signed with wallet) | — |
sell | token_address, holdings_percentage, target, slippage | expiry_hours (default 144), wallet_address |
buy | token_address, amount, target, slippage | expiry_hours, wallet_address |
trailing_sell | token_address, holdings_percentage, trail, slippage | expiry_hours |
trailing_buy | token_address, amount, trail, slippage | expiry_hours |
twap_sell | token_address, frequency, duration, amount or holdings_percentage | slippage (default 500) |
twap_buy | token_address, frequency, duration, amount (SOL lamports) | slippage (default 500) |
limit_twap_sell | token_address, target, frequency, duration, amount or holdings_percentage | slippage, expiry_hours |
limit_twap_buy | token_address, target, amount, frequency, duration | slippage, expiry_hours |
trailing_twap_sell | token_address, trail, frequency, duration, amount or holdings_percentage | slippage, expiry_hours |
trailing_twap_buy | token_address, trail, amount, frequency, duration | slippage, expiry_hours |
limit_trailing_sell | token_address, target, trail, amount or holdings_percentage | slippage, expiry_hours |
limit_trailing_buy | token_address, target, trail, amount | slippage, expiry_hours |
limit_trailing_twap_sell | token_address, target, trail, frequency, duration, amount or holdings_percentage | slippage, expiry_hours |
limit_trailing_twap_buy | token_address, target, trail, amount, frequency, duration | slippage, expiry_hours |
check_order | order_id | — |
list_orders | — | wallet_address |
cancel_order | order_id | — |
extend_order | order_id, expiry_hours (max 336) | — |
expiry_hours: default 144, max 336. Conservative client pacing: REST ≤ 2 req/s sustained (burst ≤ 5), WS mutating actions ≤ 5 msg/s. Exponential backoff on 429/5xx.
challenge with a nonce on connect. You must sign the nonce with your wallet private key and send it back in the register message. Without authenticated: true in the registered reply, all order actions are rejected.
wss://api.traderouter.ai/ws{"type": "challenge", "nonce": "…", "message": "…"}{"action": "register", "wallet_address": "<PUBKEY>", "signature": "<base58>"}{"type": "registered", "wallet_address": "…", "authenticated": true}authenticated: true may you send order actions.register without a valid signature after a challenge results in authenticated: false and all order actions are rejected with an error.
On disconnect: reconnect, receive a new challenge with a new nonce, sign and re-register. Then check for any pending order_filled messages (server queues fills that arrived while you were offline). Use the staleness check on all fills after reconnect.
The server signs every order_filled (and optionally order_created) with Ed25519. Clients must verify server_signature before signing and submitting the swap tx.
Current server signing public key (base58 Ed25519, hardcode this):
EXX3nRzfDUvbjZSmxFzHDdiSYeGVP1EGr77iziFZ4Jd4
Override via env var TRADEROUTER_SERVER_PUBKEY when using the reference MCP. For key rotation, a second key is published via TRADEROUTER_SERVER_PUBKEY_NEXT — try the current key first; if it fails, try the next; if the next key succeeds, the server has rotated, update your primary key. Rotation announcements at api.traderouter.ai/security.
already_dispatched is true: skip sign/submit entirely (idempotent ack, fill was already sent).order_id, order_type, status, token_address, entry_mcap, triggered_mcap, filled_mcap, target_mcap, triggered_at, filled_at, data. Serialize with sorted keys, no extra whitespace: e.g. json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).server_signature (base58 Ed25519) over that digest using the hardcoded trust anchor.data.swap_tx (base58 → sign → base64) and POST /protect.When order_created includes both params_hash and server_signature, the server is committing to the order parameters. Verification is optional but recommended.
Canonical params string (pipe-delimited):
| Order type | Canonical string |
|---|---|
| Limit sell / buy | order_id|token_address|order_type|target_bps|slippage|expiry_hours|amount|holdings_percentage |
| Trailing sell / buy | order_id|token_address|order_type|trail_bps|slippage|expiry_hours|amount|holdings_percentage |
For buy: holdings_percentage=0. For sell: amount=0. Compute params_hash = SHA256(canonical_string.encode("utf-8")).hexdigest(). Server signs SHA256(params_hash.encode("utf-8")) with Ed25519.
The server may require a valid server_signature on every order_filled (MCP flag: TRADEROUTER_REQUIRE_SERVER_SIGNATURE, default true). If a fill arrives without a signature when one is required, reject the fill and do not sign/submit.
Report security issues to security@traderouter.ai.
already_dispatched is true, skip sign/submit and log. data or data.swap_tx may be absent. This is an idempotent ack — the fill was already submitted.server_signature against the hardcoded trust anchor (see Security section above). If verification fails or signature is missing when required, log and do not sign/submit.order_id — use for all logging and correlation.data.swap_tx (base58 unsigned transaction).VersionedTransaction.POST /protect with { "signed_tx_base64": "…" }.order_id + returned signature together for audit trail.sol_balance_post and token_balances to update holdings state.Apply to every fill, not only after reconnect. If both triggered_mcap and filled_mcap are present and filled_mcap > 0:
if triggered_mcap / filled_mcap < 0.85:
# fill is stale — consider skipping (do not sign/submit)
Divide-by-zero guard: if filled_mcap is 0 or null, skip the ratio check entirely — the fill is not stale by this criterion. filled_mcap can be 0 or null on valid fills; do not reject fills based on this field alone.
Client Server
│─── connect wss://api.traderouter.ai/ws ──►│
│◄── {"type":"challenge","nonce":"…"} │ server sends challenge on connect
│─── {"action":"register", │
│ "wallet_address":"WALLET", │
│ "signature":"<base58>"} ──────►│ sign nonce with wallet private key
│◄── {"type":"registered","authenticated":true} │
│◄── {"type":"order_filled",…} │ pending queued fills, if any
│─── {"action":"sell",…} ──────────────────►│
│◄── {"type":"order_created",…} ─────────── │
│ │ server polls mcap every ~5s
│◄── {"type":"order_filled","data":{"swap_tx":"…"}} │ target crossed
│ verify server_signature │
│ sign swap_tx → POST /protect │
│─── {"action":"list_orders"} ─────────────►│
│◄── {"type":"order_list",…} │
│─── {"action":"cancel_order",…} ──────────►│
│◄── {"type":"order_cancelled",…} │
│◄── {"type":"heartbeat"} │ ignore, keepalive
{"type":"challenge","nonce":"…","message":"…"}
{"type":"registered","wallet_address":"…","authenticated":true}
challenge is the first message on connect. Always respond with register including a valid signature. Only proceed when registered.authenticated is true.
{
"type": "order_created",
"order_id": "uuid",
"order_type": "sell | buy | trailing_sell | trailing_buy | limit_twap_sell | limit_twap_buy | trailing_twap_sell | trailing_twap_buy | limit_trailing_sell | limit_trailing_buy | limit_trailing_twap_sell | limit_trailing_twap_buy",
"token_address": "string",
"entry_mcap": number,
"target_mcap": number,
"target_bps": number | null — limit orders,
"trail_bps": number | null — trailing orders,
"slippage": integer,
"expiry_hours": integer,
"amount": integer,
"holdings_percentage": integer,
"params_hash": "sha256 hex — canonical params commitment",
"server_signature": "base58 — Ed25519 over params_hash (when present, verify)"
}{
"type": "order_filled",
"order_id": "uuid",
"order_type": "sell | buy | trailing_sell | trailing_buy",
"status": "success | trigger_failed | error",
"token_address": "string",
"entry_mcap": number,
"triggered_mcap": number,
"filled_mcap": number | null — can be 0/null on valid fills,
"target_mcap": number,
"triggered_at": number — unix timestamp,
"filled_at": number — unix timestamp,
"server_signature": "base58 — Ed25519 over canonical payload digest",
"already_dispatched": boolean — true = idempotent ack, skip sign/submit,
"dispatched_at": number | null,
"target_bps": number | null — limit orders,
"trail_bps": number | null — trailing orders,
"data": {
"swap_tx": "base58 unsigned transaction"
…
} — absent when already_dispatched is true
}{"type":"order_status", "order_id":"…", "status":"…"}
{"type":"order_list", "orders":[…]}
{"type":"order_cancelled", "order_id":"…"}
{"type":"order_extended", "order_id":"…"}
{
"type": "twap_order_created",
"order_id": "uuid",
"order_type": "twap_buy | twap_sell",
"token_address": "string",
"pair_address": "string",
"frequency": integer,
"duration": integer,
"interval_seconds": number,
"amount_per_execution": integer,
"original_amount": integer,
"slippage": integer,
"expires_at": number,
"holdings_percentage": integer | null
}{
"type": "twap_execution",
"order_id": "uuid",
"order_type": "twap_buy | twap_sell",
"status": "success | error",
"token_address": "string",
"execution_num": integer,
"executions_total": integer,
"executions_remaining": integer,
"next_execution_at": number | null,
"server_signature": "base58",
"data": { "swap_tx": "base58", … } | absent,
"error": string | absent
}
Verify server_signature before signing and submitting data.swap_tx. When status is error, error is set and data may be absent; still advance to next slice.
{"type":"twap_order_completed", "order_id":"…", "order_type":"…", "token_address":"…", "executions_completed": N, "status":"completed"}
{"type":"twap_order_cancelled", "order_id":"…", "status":"cancelled"}
{"type":"error", "message":"…"}
{"type":"heartbeat"} — ignore, keepalive
Uses tweetnacl for Ed25519 signing and bs58 for base58 encoding. Sign the challenge nonce, include the signature in register, and only send order actions after authenticated: true.
import WebSocket from 'ws';
import bs58 from 'bs58';
import nacl from 'tweetnacl';
const wallet = /* Keypair from @solana/web3.js */;
const ws = new WebSocket("wss://api.traderouter.ai/ws");
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'challenge') {
// Sign the nonce (UTF-8 bytes) with the wallet's Ed25519 private key
const sigBytes = nacl.sign.detached(
Buffer.from(msg.nonce, 'utf-8'),
wallet.secretKey // 64-byte secretKey from Keypair
);
ws.send(JSON.stringify({
action: 'register',
wallet_address: wallet.publicKey.toBase58(),
signature: bs58.encode(sigBytes),
}));
}
if (msg.type === 'registered') {
if (!msg.authenticated) {
console.error('authentication failed — check private key');
return;
}
// Now safe to place orders
ws.send(JSON.stringify({
action: 'sell',
token_address: 'TOKEN_MINT',
holdings_percentage: 10000,
slippage: 1500,
target: 20000, // take-profit at 2× current mcap
expiry_hours: 144,
}));
}
if (msg.type === 'order_created') {
console.log('order placed:', msg.order_id);
}
if (msg.type === 'order_filled') {
// 1. Skip if already dispatched
if (msg.already_dispatched) return;
// 2. Verify server_signature against hardcoded trust anchor
// 3. Sign msg.data.swap_tx (base58 → deserialize → sign → base64)
// 4. POST /protect with signed_tx_base64
}
});
import asyncio, json, base58, hashlib, websockets
from solders.keypair import Keypair
keypair = Keypair.from_base58_string("YOUR_PRIVATE_KEY_B58")
async def main():
async with websockets.connect("wss://api.traderouter.ai/ws") as ws:
async for raw in ws:
msg = json.loads(raw)
if msg.get("type") == "challenge":
nonce = msg["nonce"]
sig = keypair.sign_message(nonce.encode("utf-8"))
await ws.send(json.dumps({
"action": "register",
"wallet_address": str(keypair.pubkey()),
"signature": base58.b58encode(bytes(sig)).decode(),
}))
elif msg.get("type") == "registered":
if not msg.get("authenticated"):
print("authentication failed — check private key")
return
await ws.send(json.dumps({
"action": "buy",
"token_address": "TOKEN_MINT",
"amount": 100000000, # 0.1 SOL
"slippage": 1500,
"target": 5000, # dip-buy at 50% of current mcap
"expiry_hours": 72,
}))
elif msg.get("type") == "order_filled":
if msg.get("already_dispatched"):
continue
# Verify server_signature, then sign and POST /protect
print("fill received:", msg.get("order_id"))
asyncio.run(main())
Call this before signing or submitting any fill. Uses tweetnacl for Ed25519 verification and Node's built-in crypto for SHA-256. Mirrors the reference implementation in trader_mcp.py.
import nacl from 'tweetnacl';
import bs58 from 'bs58';
import { createHash } from 'crypto';
// Hardcoded trust anchor — never fetch from the server at runtime
const SERVER_PUBKEY = bs58.decode('EXX3nRzfDUvbjZSmxFzHDdiSYeGVP1EGr77iziFZ4Jd4');
const SERVER_PUBKEY_NEXT = process.env.TRADEROUTER_SERVER_PUBKEY_NEXT
? bs58.decode(process.env.TRADEROUTER_SERVER_PUBKEY_NEXT)
: null;
function verifyOrderFilled(msg) {
const { server_signature, already_dispatched } = msg;
// Step 1: idempotency — skip entirely if already dispatched
if (already_dispatched) return { ok: true, skip: true };
// Step 2: require signature
if (!server_signature) {
return { ok: false, reason: 'missing server_signature' };
}
// Step 3: build canonical payload — only keys present and not null
const CANONICAL_KEYS = [
'order_id', 'order_type', 'status', 'token_address',
'entry_mcap', 'triggered_mcap', 'filled_mcap', 'target_mcap',
'triggered_at', 'filled_at', 'data',
];
const payload = {};
for (const key of CANONICAL_KEYS) {
if (msg[key] !== undefined && msg[key] !== null) payload[key] = msg[key];
}
// Step 4: canonical JSON — sorted keys, no whitespace
const canonical = JSON.stringify(payload, Object.keys(payload).sort());
// Step 5: SHA-256 digest of UTF-8 bytes
const digest = createHash('sha256').update(Buffer.from(canonical, 'utf-8')).digest();
// Step 6: verify Ed25519 — try current key, then rotation key
const sigBytes = bs58.decode(server_signature);
const keysToTry = [SERVER_PUBKEY, SERVER_PUBKEY_NEXT].filter(Boolean);
for (const pubkey of keysToTry) {
if (nacl.sign.detached.verify(digest, sigBytes, pubkey)) return { ok: true };
}
return { ok: false, reason: 'signature verification failed' };
}
// Usage in order_filled handler:
ws.on('message', async (raw) => {
const msg = JSON.parse(raw);
if (msg.type !== 'order_filled') return;
const result = verifyOrderFilled(msg);
if (result.skip) {
console.log('already_dispatched — skipping', msg.order_id);
return;
}
if (!result.ok) {
console.error('fill rejected:', result.reason, msg.order_id);
return; // do NOT sign or submit
}
// Staleness check
const { triggered_mcap, filled_mcap } = msg;
if (filled_mcap != null && filled_mcap > 0 && triggered_mcap / filled_mcap < 0.85) {
console.warn('stale fill — skipping', msg.order_id);
return;
}
// Safe to sign and submit
const signedBase64 = signVersionedTx(msg.data.swap_tx); // base58 → sign → base64
const protect = await fetch('https://api.traderouter.ai/protect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signed_tx_base64: signedBase64 }),
signal: AbortSignal.timeout(30000),
}).then(r => r.json());
console.log('submitted', msg.order_id, '→', protect.signature);
});
Uses cryptography and base58. Identical logic to the MCP server's _verify_order_filled_signature.
import json, hashlib, base58
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
# Hardcoded trust anchor — never fetch from the server at runtime
SERVER_PUBKEY_B58 = "EXX3nRzfDUvbjZSmxFzHDdiSYeGVP1EGr77iziFZ4Jd4"
CANONICAL_KEYS = [
"order_id", "order_type", "status", "token_address",
"entry_mcap", "triggered_mcap", "filled_mcap", "target_mcap",
"triggered_at", "filled_at", "data",
]
def verify_order_filled(msg: dict, server_pubkey_b58: str = SERVER_PUBKEY_B58) -> bool:
"""Returns True if signature is valid. Raises nothing — returns False on any failure."""
sig_b58 = msg.get("server_signature")
if not sig_b58:
return False # missing — reject when REQUIRE_SERVER_SIGNATURE=true
# Build canonical payload — only keys present and not null
payload = {k: msg[k] for k in CANONICAL_KEYS if k in msg and msg.get(k) is not None}
# Canonical JSON: sorted keys, no extra whitespace, ASCII-safe
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
digest = hashlib.sha256(canonical.encode("utf-8")).digest()
try:
Ed25519PublicKey.from_public_bytes(
base58.b58decode(server_pubkey_b58)
).verify(base58.b58decode(sig_b58), digest)
return True
except (InvalidSignature, ValueError):
return False
# Usage in fill handler:
async def handle_order_filled(msg: dict):
if msg.get("already_dispatched"):
print("already_dispatched — skipping", msg.get("order_id"))
return
if not verify_order_filled(msg):
print("fill rejected: signature invalid", msg.get("order_id"))
return # do NOT sign or submit
# Staleness check
triggered = msg.get("triggered_mcap")
filled = msg.get("filled_mcap")
if filled and filled > 0 and triggered / filled < 0.85:
print("stale fill — skipping", msg.get("order_id"))
return
# Safe to sign and submit
signed_b64 = sign_tx_b58(msg["data"]["swap_tx"]) # base58 → sign → base64
protect = await post("/protect", {"signed_tx_base64": signed_b64})
print("submitted", msg.get("order_id"), "→", protect.get("signature"))
# 1. Build unsigned tx
curl -X POST https://api.traderouter.ai/swap \
-H "Content-Type: application/json" \
-d '{"wallet_address":"WALLET","token_address":"MINT","action":"sell","holdings_percentage":10000,"slippage":1500}'
# 2. Decode the returned swap_tx (base58) → sign with wallet → encode as base64
# 3. Submit signed tx via MEV-protected lane (30s timeout)
curl -X POST https://api.traderouter.ai/protect \
-H "Content-Type: application/json" \
-d '{"signed_tx_base64":"YOUR_SIGNED_BASE64_TX"}'
# 4. Scan holdings (set 100s timeout — this endpoint is slow)
curl -X POST https://api.traderouter.ai/holdings --max-time 100 \
-H "Content-Type: application/json" \
-d '{"wallet_address":"WALLET"}'
| Issue | Fix |
|---|---|
/holdings times out | Set HTTP timeout to at least 100 seconds. |
/protect hangs | Set a 30s timeout. On timeout, check tx status via RPC before retrying — tx may have landed. |
/protect returns 503 | Fall back to direct RPC submission (no MEV protection). You lose priority lane but tx still goes through. |
422 from /swap | Invalid payload — likely mixed buy/sell params. Sell needs holdings_percentage; buy needs amount. Never send both. |
| "Error running simulation" | Route is unsellable now (dead pool, zero balance, or no route). Put token on cooldown; avoid retry loops. |
| Swap fails on-chain | Increase slippage (1500–2500 for memecoins). Check SOL balance for fees. Verify pool exists. |
No order_filled received | Check: (1) {"type":"registered"} was received, and (2) authenticated: true is set. A session that registered without a valid signature receives registered with authenticated: false and will never receive fills. Wallet must match the signing key. |
| WebSocket disconnects | Reconnect, re-register with a signature on the new challenge, then check for pending fills. Active orders persist server-side. |
Sell fails on token from /holdings | Apply valueNative > 0 filter. Verify balance and pool just before the sell. |
filled_mcap is 0 or null | Fill is still valid. Execute normally — mcap data is unreliable at fill time but the transaction works. |
| Order disappeared | Orders silently expire at expiry_hours. Use list_orders or check_order to verify. |
| server_signature verification fails | Check your trust anchor matches EXX3nRzfDUvbjZSmxFzHDdiSYeGVP1EGr77iziFZ4Jd4. If it does, try TRADEROUTER_SERVER_PUBKEY_NEXT — the server may have rotated. See api.traderouter.ai/security. |
| Field | Description |
|---|---|
wallet_address | Solana public key. Required for register and /swap. Optional on WS order actions once registered. |
token_address | SPL token mint. Required for orders and /swap. |
action | REST: buy | sell. WS: register, sell, buy, trailing_sell, trailing_buy, twap_buy, twap_sell, check_order, list_orders, cancel_order, extend_order. |
amount | Lamports. Buy and trailing_buy only. |
holdings_percentage | Sell and trailing_sell only. Bps (10000 = 100%). Resolved at trigger time — not at placement. |
slippage | Basis points. Allowed 100–2500. API default 500; use 1500–2500 for low-liquidity or new tokens. |
target | Bps vs current mcap at order placement time. Any positive integer; there is no allowable maximum. 20000 = mcap doubles; 5000 = mcap halves. Not relative to wallet entry price. |
trail | Trailing orders: distance in bps from peak (sell) or trough (buy). |
expiry_hours | Default 144, max 336. Expiry is silent — no server event. Not used for TWAP (duration is the order lifetime). |
frequency | TWAP only. Number of executions (1–100). |
duration | TWAP only. Total run time in seconds (min 60, max 30 days). Order expires at started_at + duration. |
amount | TWAP only. Total raw token units (sell) or SOL lamports (buy). Alternative to holdings_percentage for twap_sell. |
entry_mcap | Market cap (USD) at order creation. |
target_mcap | Absolute market cap target (USD). |
triggered_mcap | Market cap (USD) when target was crossed. |
filled_mcap | Market cap (USD) when tx was built. Can be 0/null on valid fills. Use with triggered_mcap for staleness check. |
swap_tx | Base58 unsigned Solana transaction. Decode → deserialize → sign → serialize → base64 → POST /protect. |
signed_tx_base64 | POST /protect body. Base64-encoded signed serialized transaction. Not base58. |
server_signature | Base58 Ed25519 signature from server. Verify against hardcoded trust anchor before signing/submitting any fill. |
params_hash | SHA-256 hex of canonical pipe-delimited order params. In order_created. Recompute and verify against server_signature to confirm order parameters. |
already_dispatched | Boolean. When true: idempotent ack, fill was already submitted. Skip sign/submit. data.swap_tx may be absent. |
nonce | String sent in challenge. Sign as UTF-8 bytes with wallet private key; include base58 result as signature in register. |
authenticated | Boolean in registered. Only proceed with order actions when true. |