Skip to content

AI Agents (MCP)

aspens-mcp is a Model Context Protocol server over the Aspens SDK. It lets an AI agent read a venue, place and cancel orders, and move funds in and out, within per-market limits that you set in a config file. The agent cannot change those limits.

It speaks MCP over stdio only. Your agent harness starts it as a child process on the same machine, and it opens no network listener. It trades through the same gRPC API as aspens-cli, with the same keys, so it works against any Aspens Market Stack.

1. Install

aspens-mcp lives in the SDK repository. It is built from source:

cargo install --locked --git https://github.com/aspensprotocol/sdk aspens-mcp

2. Configure

The server reads one TOML file, ~/.aspens-mcp/aspens-mcp.toml by default. Start from aspens-mcp.example.toml:

stack_url = "https://<stack>/..."   # the venue's gRPC endpoint
env_file  = "~/.aspens-mcp/.env"    # trader keys and RPC URLs, mode 0600
state_dir = "~/.aspens-mcp"         # the ledger lives here
 
allow_withdraw = false              # true registers the withdraw tool
settle_address_allowlist = []       # addresses an order may settle to, besides your own
 
[deposits]
enabled = true
# max_per_call = { "base-sepolia:0x…" = "1000" }   # keyed "network:token-address"
 
# One block per tradable market. Every field is required.
[markets."<market_id>"]
max_order_qty     = "10"        # base a single order may carry
max_daily_qty     = "100"       # base across buys and sells, per UTC day
max_order_quote   = "25000"     # quote a single buy may commit
max_daily_quote   = "250000"    # quote committed by all buys, per UTC day
max_deviation_bps = 100         # how far an order may price through the best opposite price
SettingMeaning
stack_urlThe venue's gRPC endpoint.
env_fileThe trader keys, in the variables aspens-cli reads: TRADER_PRIVKEY (EVM and Tron), TRADER_PRIVKEY_SOLANA, and ASPENS_RPC_URL_<NETWORK> per chain. The file must exist with mode 0600, or the server refuses to start. The config file itself holds no secrets.
state_dirThe server's ledger: daily usage, the orders it sent, and halts. Usage and halts survive a restart. One server per state_dir; a second refuses to start.
allow_withdrawfalse by default. The withdraw tool does not exist for the agent unless this is true.
settle_address_allowlistEmpty by default, so an order settles to the trader's own addresses. An agent may name another settle address only if it is listed here.
[deposits]With enabled = false every deposit call is refused. max_per_call caps a single deposit of a token.
[markets."<market_id>"]The limits for one market. A market with no block is not tradable through this server.

Amounts are decimal strings in the market's units. Daily usage counts submissions, not fills: an order's quantity and quote count against the UTC day once it is sent, even if it is cancelled later.

The config is read at startup only. To change a limit, edit the file and restart the server.

Pinning the venue

An optional [venue] block refuses orders, deposits and withdrawals when the venue is not the one you expect:

[venue]
expected_contracts = { "base-sepolia" = "0x…" }

With the block set, every network an action touches needs an entry (both legs of a market), and the action is refused if GetConfig reports a different trading contract for it. Without the block, the server trusts whatever answers at stack_url; account_status says so.

[venue] also accepts attestation_mrtd. Do not set it yet: a pinned measurement alone cannot verify a report, so the server fails closed and refuses every order, deposit and withdrawal.

3. Connect an agent

Any MCP client that can spawn a stdio server works. Run aspens-mcp --config <path>; serve is the default subcommand.

With Claude Code:

claude mcp add aspens -- aspens-mcp --config ~/.aspens-mcp/aspens-mcp.toml

For a client configured through JSON:

{
  "mcpServers": {
    "aspens": {
      "command": "aspens-mcp",
      "args": ["--config", "/home/you/.aspens-mcp/aspens-mcp.toml"]
    }
  }
}

Logs go to stderr. Stdout carries only JSON-RPC.

4. Tools

The server registers fifteen tools, and withdraw as a sixteenth when allow_withdraw = true. Amounts in arguments and results are decimal strings; results carry the integer beside each one in a …Raw field.

Read

ToolArgumentsReturns
venue_confignoneThe venue's markets (legs, decimals, pair decimals, tradable here or not), chains and tokens.
top_of_bookmarket_idBest bid and best ask. A side is null when it is empty.
orderbookmarket_id, levels (default 20)Visible depth, aggregated by price, best first.
balancesnonePer token and chain: the wallet balance, the amount available on the venue, the amount locked, and native gas.
my_ordersstatus: open (default), closed or allThe orders this server sent, with the venue's record of each: state, filled and remaining quantity, fills.
get_orderorder_idOne of those orders, with its fills.
my_fillsmarket_idYour fills on a market, as maker and as taker, newest last. Covers open orders and orders closed in the last 7 days.
account_statusnoneYour addresses per curve, which keys are loaded, whether the venue is reachable, whether trading is halted and why, and whether the venue is pinned.
limitsnoneThe configured limits per market, with today's usage and remaining budget.
venue_attestationnoneThe venue's attestation report for a fresh nonce, and whether it was verified.

Write

Every write tool checks every limit before anything is signed. A refusal names each rule that blocked it. Unknown arguments are an error, never ignored.

ToolArgumentsEffect
place_limit_ordermarket_id, side, qty, price; optional post_only, hidden, settle_base_address, settle_quote_addressPlaces a limit order.
place_marketable_ordermarket_id, side, qty; optional slippage_bps (default 50)Takes liquidity with a limit order priced at the best opposite price, moved against you by slippage_bps.
cancel_orderorder_id; market_id and side only for an order this server did not sendCancels an order. One that is not live is reported as already_gone. Allowed while halted.
depositnetwork, token (symbol), amountMoves a token from your wallet into the venue.
withdrawnetwork, token (symbol), amountMoves a token from the venue to your own address on that network. A call can take about two minutes.
haltoptional reasonStops new orders, deposits and withdrawals.

There are no raw market orders and no named-counterparty fills. Those parameters are absent from the tool schemas.

5. What the limits enforce

  • Size per order. A buy never commits more quote than max_order_quote, and no order carries more base than max_order_qty.
  • Size per day. max_daily_qty and max_daily_quote cap what is submitted in one UTC day. The usage record is written to disk before the order is signed.
  • Price. An order may price at most max_deviation_bps through the venue's best opposite price at that moment. For place_marketable_order, slippage beyond that is refused.
  • Markets. Only markets with a [markets] block are tradable.
  • Destinations. Withdrawals go to the trader's own address only. Settle addresses must be on the allowlist.

The price check reads the venue's own book. On a thin book someone else can set the best bid or ask, and the check follows them. The size caps are what hold, so set them to what you can afford to lose to a bad fill. A resting order can also fill later at its own price after the market has moved; that is the ordinary risk of a limit order.

6. Halt and resume

halt is a kill switch the agent can pull and cannot release. It is written to the ledger, so it survives a restart. Cancels stay allowed while halted, since a cancel only reduces exposure.

To lift a halt, stop the server and run:

aspens-mcp resume --config ~/.aspens-mcp/aspens-mcp.toml

resume refuses while a server holds the same state_dir.

7. Order states

my_orders and get_order report the venue's order status record for each order:

StateMeaning
open, partially_filled, filled, cancelled, expiredAs named.
dropped_by_restartThe venue restarted while the order rested.
tradedClosed by fills. The venue cannot recover the order's initial size, so no remaining quantity is shown, and its fills have side: null.
rejectedThe venue refused the send.
unknownThe venue has no record of the order.

An order's id is derived from the exact order and recorded before it is signed. If a send gets no answer, the order is reported unknown under that id, get_order asks the venue about it, and its real state replaces unknown once the venue reports it. The server never sends the order a second time.

The venue reports a closed order for 7 days.

Reading order status

Order status is an owner-signed read. At startup the server makes an Ed25519 delegate key, keeps it in memory only, and has each trader wallet sign a read-only delegation to it for 24 hours. A leaked delegate key can read the owner's order statuses until that delegation expires, and there is no early revocation. It can never trade, cancel or withdraw.

Hidden orders

place_limit_order takes hidden: true. A hidden order stays out of the public book and its fills print with your side redacted. It goes through every limit a visible order does, and get_order, my_orders and my_fills still show it to you.

8. Behaviour to know about

  • Order status can trail a send by a moment. get_order straight after place_limit_order may not yet show a taker fill that the send response did.
  • halt and cancel_order can wait up to about 60 s behind an order whose send is still in flight.
  • When the client disconnects mid-send, the server waits up to 70 s for the send to finish and record its outcome before it exits. If the process dies first, the next start lists that order as unknown and still counts it against the day's limits.
  • A venue restart that changes a market's pair decimals refuses orders on that market until aspens-mcp restarts.
  • Amounts with more decimal places than the market or token allows are rejected, not rounded.