Tutorial
Connect your own agent
Oddysey exposes a remote MCP server so your agent can read what you approved, act on it wherever you actually trade, and report the fill back. This is the part that turns a review queue into a working loop.
What this gets you
Oddysey is the supervised half of an agent workflow. The executing half is yours, and it stays yours — Oddysey never learns your venue credentials and never touches your account.
The server speaks MCP over Streamable HTTP at /api/mcp, protocol version 2025-06-18, authenticated with a bearer token you issue in the deck. It exposes thirteen tools.
Set it up
- 1
Issue a deck token
In the deck, open Settings → MCP access and create a token with a label naming the client it is for. A token looks like
odsy_followed by 64 hex characters. - 2
Point your client at the server
For clients that speak stdio — Claude Desktop, Cursor, most of them — install nothing.
npxfetches the bridge, and the token is the only thing in the config:claude_desktop_config.jsonJSON { "mcpServers": { "oddysey": { "command": "npx", "args": ["-y", "@oddysey/mcp"], "env": { "ODDYSEY_TOKEN": "odsy_..." } } } }In Claude Code the same thing is one line:
claude mcp add oddysey --env ODDYSEY_TOKEN=odsy_... -- npx -y @oddysey/mcp. Append--scope readto say what the bridge is for. It does not narrow the token — scopes are fixed here when the token is issued, and only this server enforces them — but it lets the bridge tell an expected refusal from a token that turned out narrower than you thought.Any client that can send an
Authorizationheader can skip the bridge and speak to/api/mcpdirectly. A genericmcp-remoteworks too, if you would rather not add another package to your supply chain:claude_desktop_config.json — without the bridgeJSON { "mcpServers": { "oddysey": { "command": "npx", "args": [ "-y", "mcp-remote", "https://oddysey.dev/api/mcp", "--header", "Authorization: Bearer ${ODDYSEY_TOKEN}" ], "env": { "ODDYSEY_TOKEN": "odsy_..." } } } } - 3
Shake hands
initializeHTTP POST /api/mcp authorization: Bearer odsy_... content-type: application/json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "clientInfo": { "name": "my-trading-agent", "version": "1.0.0" } } }Send a client name you will recognise later: it is recorded against anything that client reports, which is how the ledger can distinguish two agents working the same deck.
- 4
Discover the tools
tools/listJSON {"jsonrpc":"2.0","id":2,"method":"tools/list"}Batched requests are answered as a batch, and notifications get
202with no body. The server never initiates messages, so aGETfor a server-sent stream is refused with405rather than left hanging open.
Tool reference
the nine core tools · all scoped to the token's owner
| TOOL | ARGUMENTS | WHAT IT DOES |
|---|---|---|
get_portfolio | — | Holdings at the latest quotes, plus market. Check market.live before reasoning from the prices. |
list_watches | — | Every watch on this deck. |
create_watch | symbol, thresholdPct, action, note? | Arms a new watch. Authorises nothing on its own. |
set_watch_status | watchId, status | Pause or resume. Returns changed:false if it was already in that state. |
list_proposals | status? | Filter by pending, approved, executed, rejected, or all. approved is the one your agent wants. |
get_proposal | proposalId | One proposal in full, including rationale and both allocations. |
list_ledger | limit? (1–200, default 50) | The audit trail, newest first. |
evaluate_watches | — | Runs the sweep now instead of waiting. Watches in cooldown are skipped. |
record_execution | proposalId, venue, orderRef, note? | Reports a fill and closes out an approved proposal. Refused for anything not approved. |
Tools get added between releases — there are four more for tokenized real-world assets than this table lists. Treat it as a snapshot and tools/list against your own token as the current answer: it comes back already narrowed to what that token is scoped for, which is why nothing that connects to this server needs to keep its own copy of the list.
A sane agent loop
# a sane agent loop against Oddysey
1. get_portfolio
└─ if market.live is false, say so and stop reasoning from the prices
2. list_proposals { status: "approved" }
└─ nothing to do? exit. do not go looking for pending ones to act on.
3. for each approved proposal:
get_proposal { proposalId } # full rationale + allocations
… execute on your own rail …
record_execution { proposalId, venue, orderRef, note }
4. list_ledger { limit: 20 } # confirm what you just wrote landedThe server hands your client instructions at initialize that say much the same thing: this is a supervision layer, not an execution venue; approval is a human’s job; and when market.live is false you should say so rather than reason from placeholder prices.
What calls cost
The handshake is free and reads are a fraction of a cent. Nothing here is billed to a card — a deck spends a credit balance, and an agent that runs it dry can refill it itself.
per call · USDC, charged before the tool runs
| PRICE | CALLS | WHY |
|---|---|---|
Free | initialize, ping, tools/list | An agent should never pay to find out what it may do. |
$0.0002 | get_portfolio, list_watches, list_proposals, get_proposal, list_ledger | Polling for approved work is the behaviour we want, not the behaviour we tax. |
$0.002 | create_watch, set_watch_status, record_execution | These land in someone's audit trail. |
$0.005 | evaluate_watches | Fans out to a quote provider and a model, on Oddysey's side. |
An unknown tool name costs nothing — it comes back as METHOD_NOT_FOUND, and billing for a refusal would be indefensible. Every priced response carries x-oddysey-credits: the remaining balance in atomic USDC units, so your client can watch itself drain and top up before it stalls mid-loop.
When something returns 402
Out of credit, the endpoint quotes a price instead of failing. The body is an x402 challenge — the same shape any x402 client already knows how to satisfy.
HTTP/1.1 402 Payment Required
content-type: application/json
{
"x402Version": 1,
"error": "This request costs $0.0020 and your deck is out of credit. Pay $1.00 to continue.",
"accepts": [{
"scheme": "exact",
"network": "eip155:4663",
"maxAmountRequired": "1000000",
"resource": "https://oddysey.dev/api/mcp",
"payTo": "0x9C08b14A39bb9165B2F793Ce5c70fAC0363622b3",
"asset": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
"maxTimeoutSeconds": 120,
"extra": { "name": "Global Dollar", "version": "1" }
}]
}
# retry the identical request with a signed payment
POST /api/mcp
authorization: Bearer odsy_...
x-payment: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLC4uLn0=Sign the quoted amount as an EIP-3009 authorisation against the domain in extra, base64 it into an x-payment header, and send the identical request again. Oddysey verifies and settles through a facilitator — it never holds a key for this and never sees yours — credits your deck, then runs the call. The settlement receipt comes back in x-payment-response.
The terms above are the live ones: USDG on Robinhood Chain, settled by Aeron’s facilitator. A payer needs USDG and nothing else — settlement is relayed, so the gas is not yours. And maxAmountRequired is exact in this dialect: EIP-3009 moves that value, so overpaying is refused the same as underpaying.
Prefer to buy in bulk? POST /api/credits/topup with { "usd": 25 } and the same deck token takes one payment for a whole pack. And if a deployment has no payment address configured, metering is inert: no call is ever priced and no 402 is ever issued.
Token hygiene
- One token per client, labelled. Revoking then becomes surgical rather than an outage for every agent you run.
- Watch
last usedin the settings panel. A token being used while the client that owns it is idle is the signal you want to catch. - Revocation is immediate — the next request with that token gets
401. - A token grants exactly what the nine tools above do, on one operator’s data. It cannot approve, cannot spend, and cannot reach another deck.
When something returns 401
Three causes, in order: the header is missing or malformed (it must be Authorization: Bearer odsy_…), the token was revoked, or the token was never valid. The response carries a WWW-Authenticate challenge, and the server does not distinguish between an unknown token and a revoked one — that distinction would be useful to an attacker and to nobody else.