Production engine
The same fill logic, drawdown rails and recorded venue prices a person trades against. Not a sandbox reimplementation.
Last updated
A REST API for autonomous agents. Requests carry a bearer key; orders run on the production evaluation engine with simulated execution, against agent sessions that never touch funded capital or an on-chain payout.
Every endpoint, with its real request shape and a response you can read. DEMO answers from a recorded example of what the endpoint returns, without leaving your browser; LIVE calls the API with a key you paste in. A key typed here stays in the tab — it is never stored, never logged, and never sent anywhere but this API.
/api/v1/agent/ordersscope: tradePlace an order against an agent session.
Nothing sent yet. In DEMO the response is a recorded example of this endpoint's real shape — no request leaves your browser. Switch to LIVE to call the API with your own key.
200The order was accepted and filled against the recorded price400The body is malformed, or clientOrderId is missing409The session is closed, or the key was reused with a different payload401The key is missing, unknown, revoked or expired403The key does not hold the scope this endpoint needs404No such agent account — or it is not yours. The two answers are identical on purpose429Rate limited. `Retry-After` says when to come backcurl -X POST "https://app.cats.fund/api/v1/agent/orders" \
-H "Content-Type: application/json" \
-d '{"accountId":"2894802230932904885589274625217197696331749616641014100986439600197828240999","symbol":"BTC","side":"long","intent":"open","size":"0.01","clientOrderId":"console-1788896751418"}'What an agent actually does: read a price, decide, place an order, watch the fill. Replayed over 90 real recorded one-minute BTC candles — nothing leaves your browser.
Press Run agent. Every 420ms the loop reads the next candle and runs four steps:
POST /api/v1/agent/orders90 real recorded one-minute BTC candles. Nothing leaves your browser.
One MCP server. Claude, OpenAI and Gemini all speak the protocol natively, so the same 5 tools appear in every host from one URL — no adapter per provider, and nothing to keep in step when the API changes.
https://app.cats.fund/api/v1/mcp

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.beta.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
betas: ["mcp-client-2025-11-20"],
mcp_servers: [
{
type: "url",
url: "https://app.cats.fund/api/v1/mcp",
name: "cats_fund",
authorization_token: process.env.CATS_API_KEY,
},
],
// The server definition alone is rejected — the toolset is what
// enables the tools. Here it also denies the one tool that moves
// money, so the agent can read the book all day and cannot trade.
tools: [
{
type: "mcp_toolset",
mcp_server_name: "cats_fund",
configs: {
"cats_place_orders": {enabled: false},
},
},
],
messages: [
{
role: "user",
content: "What is my open exposure, and how is P&L trending?",
},
],
});
console.log(response.content);cats_place_orders changes state, and it is published with readOnlyHint: false so a host can tell. Anthropic can disable it outright from the toolset config; OpenAI can leave it off the never-approve list so it prompts. Both are shown above.2025-11-25. One POST, one JSON answer — the server holds no session, so a key is all a call carries. The deprecated SSE transport is not offered, and Gemini would refuse it anyway.The rest of the API, four ways in. Everything below is on the page at once without JavaScript — the tabs only shorten the scroll.
The same fill logic, drawdown rails and recorded venue prices a person trades against. Not a sandbox reimplementation.
No order reaches an external venue. Fills are computed against recorded prices and stored, exactly as an evaluation's are.
An agent session cannot acquire a certificate, enter an epoch, or produce a payout — those live in tables an agent id cannot appear in.
Authorization: Bearer <secret>trade to place orders, read for everything else.429 carries Retry-After.Keys are requested and approved, never self-served. You generate the secret yourself, so we never hold it.
POST /api/v1/keys/request, signed in with your wallet.
An operator approves it. Nobody self-serves a key.
Generate 32 random bytes and send only their SHA-256. The secret never leaves your machine — so we cannot lose it, leak it, or show it to an operator, and a missing response is safe to retry with the same hash.
SECRET="ck_live_$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
HASH=$(printf '%s' "$SECRET" | shasum -a 256 | cut -d' ' -f1)
curl -X POST https://app.cats.fund/api/v1/keys/claim \
-H "Content-Type: application/json" \
--cookie "cats_session=..." \
-d "{"requestId":"$REQUEST_ID","keyHash":"$HASH"}"Enough to run a strategy, not just to buy and sell. Every field below is recorded as a timestamped event and replayed by the same engine that judges a human evaluation — so a stop is part of the audit trail, not a note your bot keeps to itself.
intent: "open" with a size. Fills at the recorded venue price for the moment it was submitted. There is no price field on an order — you cannot name your own fill, and the engine has no discretion to give you a better one.triggerPrice and triggerWhen. Whether that makes it a limit or a stop follows from where the price sat when you submitted it, and that is recorded rather than inferred later — a replay must not have to guess what the market looked like.takeProfit and stopLoss on the opening order, or alone with intent: "protect". Three states: a value sets it, null clears it, and omitting it leaves it alone — which is what lets you move a stop without restating a target.protection{trigger, price, size} to scale out in parts. A ladder replaces the two scalar levels outright rather than merging with them: an order carrying both is one instruction, not two.intent: "cancel" with cancels naming the resting order. The cancel is its own event, so the record shows an order placed and then withdrawn — which is what happened, and reads differently from the order never existing.A protect order carries no size and never fills. It exists so that setting a stop is an ordered, timestamped event like everything else the engine replays — levels held anywhere else could not be reconstructed, and there would be no way to say whether a stop was in place before a given tick.
"size": "0.01" means one hundredth of a unit — not engine units. Sending 1000000 for one BTC is read literally, and the order is refused or filled at a size nobody intended.
Every order needs a clientOrderId. It is required rather than offered, because an agent retrying an ambiguous timeout has no human to notice the duplicate.
409. The key names an order that already exists.A 404 means the account does not exist or is not yours — deliberately the same answer, so the API cannot be walked to discover which account ids are real.