traderouter.ai

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.

REST https://api.traderouter.ai WS wss://api.traderouter.ai/ws Auth: none MCP server — Claude, Cursor, etc.
For AI agents & machines:
Trust signals (verifiable):
▸  Changelog v1.4.0
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.

Trade Router MCP

A Model Context Protocol server for TradeRouter.ai — Solana swap & limit order engine. Use it from Claude Desktop, Cursor, or any MCP host.

Requirements

Installation

# npm (Node.js)
npx -y @traderouter/trade-router-mcp

# PyPI (Python equivalent, runs the same 21 tools)
uv add traderouter-mcp
traderouter-mcp

Claude Desktop setup

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.

Use with ElizaOS / LangChain / Solana Agent Kit

Both ElizaOS and LangChain ship official generic MCP-client adapters — no Trade Router-specific code needed. See github.com/TradeRouter/cookbook:

OSConfig path
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json

Environment variables

VariableRequiredDescription
TRADEROUTER_PRIVATE_KEYSolana wallet private key in base58 format. Read once at MCP server start, used for local signing, never transmitted. See SECURITY.md.
TRADEROUTER_DRY_RUNDefault 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_URLRPC endpoint. Defaults to https://api.mainnet-beta.solana.com
TRADEROUTER_SERVER_PUBKEYServer public key for signature verification
TRADEROUTER_SERVER_PUBKEY_NEXTNext server public key for key rotation
TRADEROUTER_REQUIRE_SERVER_SIGNATUREVerify server signatures on fills. Defaults to true
TRADEROUTER_REQUIRE_ORDER_CREATED_SIGNATUREVerify server signatures on order creation. Defaults to true

Available tools

ToolDescription
get_wallet_addressGet the configured wallet address
build_swapBuild an unsigned swap transaction
submit_signed_swapSubmit a manually signed transaction
auto_swapBuild and auto-sign a swap in one step
get_holdingsGet token holdings for a wallet
get_mcapGet market cap data for token address(es)
get_flex_cardGet flex trade card PNG URL for wallet + token
place_limit_orderPlace a limit buy or sell order
place_trailing_orderPlace a trailing buy or sell order
place_twap_orderPlace a TWAP buy or sell order (time-weighted slices)
list_ordersList all active orders for a wallet
check_orderCheck the status of an order
cancel_orderCancel an active order
extend_orderExtend an order's expiry
connect_websocketConnect and register WebSocket for a wallet
connection_statusGet current WebSocket connection status
get_fill_logGet log of filled orders
POST /swap

Build an unsigned Solana swap transaction. Returns base58 serialized tx — client signs, then submits via POST /protect.

Sell uses 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.

Request

{
  "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)
}

Response (success)

{
  "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
  }
}

Error

{ "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.

POST /protect

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.

⚠ Encoding: /swap returns swap_tx as base58. /protect expects base64. You must decode base58 → deserialize → sign → serialize → encode base64. Never send base58 directly to /protect.
⚠ Timeout: Set a 30s client timeout on /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).

Request

{ "signed_tx_base64": "string — base64-encoded signed serialized transaction" }

Response (success)

{
  "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"
    }
  ]
}

Signing workflow (step by step)

  1. POST /swap → read data.swap_tx (base58)
  2. Decode base58 → raw bytes
  3. Deserialize as VersionedTransaction
  4. Sign with client wallet private key
  5. Re-serialize → encode as base64
  6. POST /protect with { "signed_tx_base64": "…" }
  7. On success: use sol_balance_post + token_balances to update holdings state
POST /holdings

Scan Solana wallet token holdings with liquid DEX pool info.

Slow endpoint. Set HTTP timeout to at least 100 seconds. Scans all token accounts on-chain.

Request

{ "wallet_address": "string" }

Response

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.

GET /mcap

Return market cap data for given token addresses.

Query parameters

tokens — comma-delimited list of Solana mint addresses.

Example

GET https://api.traderouter.ai/mcap?tokens=MINT1,MINT2

Response

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
  }
}
GET /flex

Generate a flex trade card PNG for a wallet and token mint.

Query parameters

wallet_address — Solana wallet pubkey. token_address — SPL token mint.

Example

GET https://api.traderouter.ai/flex?wallet_address=WALLET&token_address=MINT

Response

Image PNG (Content-Type: image/png). 400 on invalid params; 501 if flex_card_image deps not available; 500 on server error.

WS wss://api.traderouter.ai/ws

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.

Order types

ActionDirectionUse caseKey param
selltarget > 10000Take-profit (mcap doubles = 20000)holdings_percentage + target
selltarget < 10000Stop-loss (mcap halves = 5000)holdings_percentage + target
buytarget < 10000Dip buy (mcap halves = 5000)amount + target
buytarget > 10000Breakout entry (mcap doubles = 20000)amount + target
trailing_sellAuto-adjusting stop from peakholdings_percentage + trail
trailing_buyAuto-adjusting entry from troughamount + trail
twap_sellTime-weighted sell over durationamount or holdings_percentage + frequency + duration
twap_buyTime-weighted buy over durationamount + frequency + duration
limit_twap_sellLimit target then TWAP selltarget + frequency + duration
limit_twap_buyLimit target then TWAP buytarget + amount + frequency + duration
trailing_twap_sellTrail triggers then TWAP selltrail + frequency + duration
trailing_twap_buyTrail triggers then TWAP buytrail + amount + frequency + duration
limit_trailing_sellLimit then trailing stop → single swaptarget + trail
limit_trailing_buyLimit then trailing stop → single swaptarget + trail + amount
limit_trailing_twap_sellLimit then trail then TWAP selltarget + trail + frequency + duration
limit_trailing_twap_buyLimit then trail then TWAP buytarget + 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.

Order management

{"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

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 (time-weighted average price)

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.

Combo orders

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).

All actions reference

ActionRequired fieldsOptional
registerwallet_address, signature (base58 of nonce signed with wallet)
selltoken_address, holdings_percentage, target, slippageexpiry_hours (default 144), wallet_address
buytoken_address, amount, target, slippageexpiry_hours, wallet_address
trailing_selltoken_address, holdings_percentage, trail, slippageexpiry_hours
trailing_buytoken_address, amount, trail, slippageexpiry_hours
twap_selltoken_address, frequency, duration, amount or holdings_percentageslippage (default 500)
twap_buytoken_address, frequency, duration, amount (SOL lamports)slippage (default 500)
limit_twap_selltoken_address, target, frequency, duration, amount or holdings_percentageslippage, expiry_hours
limit_twap_buytoken_address, target, amount, frequency, durationslippage, expiry_hours
trailing_twap_selltoken_address, trail, frequency, duration, amount or holdings_percentageslippage, expiry_hours
trailing_twap_buytoken_address, trail, amount, frequency, durationslippage, expiry_hours
limit_trailing_selltoken_address, target, trail, amount or holdings_percentageslippage, expiry_hours
limit_trailing_buytoken_address, target, trail, amountslippage, expiry_hours
limit_trailing_twap_selltoken_address, target, trail, frequency, duration, amount or holdings_percentageslippage, expiry_hours
limit_trailing_twap_buytoken_address, target, trail, amount, frequency, durationslippage, expiry_hours
check_orderorder_id
list_orderswallet_address
cancel_orderorder_id
extend_orderorder_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.

Connection & registration

Registration is mandatory for all order actions. The server sends a 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.

Connection sequence

  1. Connect to wss://api.traderouter.ai/ws
  2. Server sends: {"type": "challenge", "nonce": "…", "message": "…"}
  3. Sign the nonce (UTF-8 bytes) with your wallet's Ed25519 private key. Encode the signature as base58.
  4. Send: {"action": "register", "wallet_address": "<PUBKEY>", "signature": "<base58>"}
  5. Server sends: {"type": "registered", "wallet_address": "…", "authenticated": true}
  6. Only after authenticated: true may you send order actions.
Private key required for WS orders. You must have the wallet's private key to sign the challenge nonce. Sending register without a valid signature after a challenge results in authenticated: false and all order actions are rejected with an error.

WebSocket lifecycle state machine

DISCONNECTED ──connect──► CHALLENGE server sends {"type":"challenge","nonce":"…"} CHALLENGE ──sign nonce; send register with signature──► REGISTERED server sends {"type":"registered","authenticated":true} REGISTERED ──place orders──► ACTIVE orders live, server monitoring mcap every ~5s ACTIVE ──disconnect──► DISCONNECTED reconnect → new challenge → re-sign → re-register (active orders persist server-side, no need to re-place)

Reconnection

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.

Server signature verification

The server signs every order_filled (and optionally order_created) with Ed25519. Clients must verify server_signature before signing and submitting the swap tx.

Trust anchor — hardcoded, never fetched

Never fetch the server public key from the same server at runtime (e.g. via a network request) to verify that server's own messages. This is a TOCTOU vulnerability. The trust anchor must be hardcoded or preconfigured out-of-band.

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.

Verifying order_filled.server_signature

  1. If already_dispatched is true: skip sign/submit entirely (idempotent ack, fill was already sent).
  2. Build canonical JSON from the message using only these keys (include only if present and not null): 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).
  3. SHA-256 the UTF-8 bytes of the canonical JSON string.
  4. Verify server_signature (base58 Ed25519) over that digest using the hardcoded trust anchor.
  5. On failure or missing signature (when required): reject the fill. Do not sign or submit.
  6. On success: sign data.swap_tx (base58 → sign → base64) and POST /protect.

Verifying order_created.server_signature (params commitment)

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 typeCanonical string
Limit sell / buyorder_id|token_address|order_type|target_bps|slippage|expiry_hours|amount|holdings_percentage
Trailing sell / buyorder_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.

REQUIRE_SERVER_SIGNATURE

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.

Responsible disclosure

Report security issues to security@traderouter.ai.

Handling order_filled

order_filled handling sequence — follow this exact order:
  1. Idempotency: if 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.
  2. Verify: verify 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.
  3. Read order_id — use for all logging and correlation.
  4. Read data.swap_tx (base58 unsigned transaction).
  5. Decode base58 → raw bytes → deserialize as VersionedTransaction.
  6. Sign with client wallet, serialize, encode as base64.
  7. POST /protect with { "signed_tx_base64": "…" }.
  8. Log order_id + returned signature together for audit trail.
  9. Use sol_balance_post and token_balances to update holdings state.

Staleness check

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.

WebSocket message flow

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

Server → client message shapes

challenge / registered
{"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.

order_created
{
  "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)"
}
order_filled
{
  "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
}
order_status, order_list, order_cancelled, order_extended
{"type":"order_status",    "order_id":"…", "status":"…"}
{"type":"order_list",      "orders":[…]}
{"type":"order_cancelled", "order_id":"…"}
{"type":"order_extended",  "order_id":"…"}
twap_order_created
{
  "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
}
twap_execution
{
  "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.

twap_order_completed, twap_order_cancelled
{"type":"twap_order_completed", "order_id":"…", "order_type":"…", "token_address":"…", "executions_completed": N, "status":"completed"}
{"type":"twap_order_cancelled", "order_id":"…", "status":"cancelled"}
error / heartbeat
{"type":"error",     "message":"…"}
{"type":"heartbeat"}   — ignore, keepalive

JavaScript — WebSocket (challenge → register → order)

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
  }
});

Python — WebSocket

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())

JavaScript — Verifying order_filled server_signature

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);
});

Python — Verifying order_filled server_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"))

curl — instant swap

# 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"}'

Troubleshooting

IssueFix
/holdings times outSet HTTP timeout to at least 100 seconds.
/protect hangsSet a 30s timeout. On timeout, check tx status via RPC before retrying — tx may have landed.
/protect returns 503Fall back to direct RPC submission (no MEV protection). You lose priority lane but tx still goes through.
422 from /swapInvalid 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-chainIncrease slippage (1500–2500 for memecoins). Check SOL balance for fees. Verify pool exists.
No order_filled receivedCheck: (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 disconnectsReconnect, re-register with a signature on the new challenge, then check for pending fills. Active orders persist server-side.
Sell fails on token from /holdingsApply valueNative > 0 filter. Verify balance and pool just before the sell.
filled_mcap is 0 or nullFill is still valid. Execute normally — mcap data is unreliable at fill time but the transaction works.
Order disappearedOrders silently expire at expiry_hours. Use list_orders or check_order to verify.
server_signature verification failsCheck your trust anchor matches EXX3nRzfDUvbjZSmxFzHDdiSYeGVP1EGr77iziFZ4Jd4. If it does, try TRADEROUTER_SERVER_PUBKEY_NEXT — the server may have rotated. See api.traderouter.ai/security.

Field reference

FieldDescription
wallet_addressSolana public key. Required for register and /swap. Optional on WS order actions once registered.
token_addressSPL token mint. Required for orders and /swap.
actionREST: buy | sell. WS: register, sell, buy, trailing_sell, trailing_buy, twap_buy, twap_sell, check_order, list_orders, cancel_order, extend_order.
amountLamports. Buy and trailing_buy only.
holdings_percentageSell and trailing_sell only. Bps (10000 = 100%). Resolved at trigger time — not at placement.
slippageBasis points. Allowed 100–2500. API default 500; use 1500–2500 for low-liquidity or new tokens.
targetBps 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.
trailTrailing orders: distance in bps from peak (sell) or trough (buy).
expiry_hoursDefault 144, max 336. Expiry is silent — no server event. Not used for TWAP (duration is the order lifetime).
frequencyTWAP only. Number of executions (1–100).
durationTWAP only. Total run time in seconds (min 60, max 30 days). Order expires at started_at + duration.
amountTWAP only. Total raw token units (sell) or SOL lamports (buy). Alternative to holdings_percentage for twap_sell.
entry_mcapMarket cap (USD) at order creation.
target_mcapAbsolute market cap target (USD).
triggered_mcapMarket cap (USD) when target was crossed.
filled_mcapMarket cap (USD) when tx was built. Can be 0/null on valid fills. Use with triggered_mcap for staleness check.
swap_txBase58 unsigned Solana transaction. Decode → deserialize → sign → serialize → base64 → POST /protect.
signed_tx_base64POST /protect body. Base64-encoded signed serialized transaction. Not base58.
server_signatureBase58 Ed25519 signature from server. Verify against hardcoded trust anchor before signing/submitting any fill.
params_hashSHA-256 hex of canonical pipe-delimited order params. In order_created. Recompute and verify against server_signature to confirm order parameters.
already_dispatchedBoolean. When true: idempotent ack, fill was already submitted. Skip sign/submit. data.swap_tx may be absent.
nonceString sent in challenge. Sign as UTF-8 bytes with wallet private key; include base58 result as signature in register.
authenticatedBoolean in registered. Only proceed with order actions when true.