Trade journal (MCP)
The read-only MCP server for your trade journal, its journal:read key, the nine tools, the three prompts, and the TypeScript client.
The trade journal server is a remote MCP (Model Context Protocol) server. It gives an AI assistant, or your own code, read-only access to one trader's journal: accounts, closed trades, fills, funding, daily equity, metrics, open positions, payouts and the account's limits. For the trader's view, see Trade journal.
The server
| Address | https://mcp.perpsfund.com |
| Transport | Streamable HTTP, stateless. Send each JSON-RPC message as a POST; the answer is JSON. There is no session and no server-to-client stream: a GET answers 405. |
| Sign-in | Authorization: Bearer <key> (or X-API-Key), on every request, initialize included. |
| Browsers | No CORS headers, as on the rest of the API: a key belongs in a server or a desktop app. |
It has no tool that places, changes, closes or cancels anything, and the reads behind the tools write nothing to your accounts.
The key
The server takes a key with the journal:read scope and no other. Create one on Analytics, under
Connect your AI, or in Settings by choosing Journal under Access.
Creating any key needs API access and a second factor: see Authentication.
- A
readortradekey is refused here with403 insufficient_scope. - A journal key is refused on every route of the trading API with
403 insufficient_scope. - A live key (
pk_live_) reads your real accounts. A sandbox key (pk_test_) reads the sandbox's paper account.
Everything else about a key is as on the trading API, because the same gate checks it: expiry, revocation, the IP
allowlist, and the rate limits, 600 per minute for a key and 2,000 per minute across
all your keys, counted together with your calls to the trading API. One HTTP request is one call. A refusal at the
gate is an HTTP error in the API's envelope, { "error": "<code>", "message": "<sentence>" }, with the codes on
Errors. Every response carries x-request-id and the X-RateLimit-* headers.
Tools
Every tool is read-only. Money is in US dollars; times are ISO 8601 in UTC. from and to are a UTC day
(2026-10-01) or an ISO 8601 time; a day as to means the whole day. A paged tool takes limit
(1 to 500, default 100) and cursor, and answers nextCursor:
pass it back as cursor for the page after, until it is null.
| Tool | Arguments | Answers |
|---|---|---|
list_accounts | none | accounts: the rows of GET /accounts. |
get_trades | accountId, from, to, limit, cursor | trades, newest first, and nextCursor. A trade is the row of GET /trades plus holdSeconds. realizedPnL is gross and fee is the round-trip fee: net is one less the other. |
get_fills | accountId, symbol, from, to, limit, cursor | fills, newest first, and nextCursor. A fill is the row of GET /fills plus accountId. |
get_funding | accountId, from, to, limit, cursor | funding, newest first, and nextCursor: id, accountId, symbol, payment (negative is a charge), rate, positionSize, hour, createdAt. |
get_daily_equity | accountId, from, to | equity, oldest first: accountId, date, equity and balance at the 00:00 UTC reset that opened the day. At most 1,000 days, the newest of the window. |
get_metrics | accountId, from, to | metrics: see below. |
get_open_positions | accountId | positions: the rows of GET /positions. |
get_payouts | accountId (optional), from, to | payouts, newest first: id, type, accountId, amount, status, network, txHash, requestedAt, paidAt. |
get_rules | accountId | The body of GET /accounts/{accountId}: balance and equity, the loss floors and the distance to each, the profit target and the distance to it, drawdown percentages, profit split, phase and status. |
A tool's answer is in the result's structuredContent, and the same JSON as text in content. A tool that
refuses answers isError: true with one line of text, <code>: <sentence>:
| Code | When |
|---|---|
account_not_found | The account id is not one of the key owner's. |
bad_date | from or to cannot be read, or from is after to. |
bad_cursor | The cursor is not one this tool gave out. |
Metrics
get_metrics computes over the closed trades of the window, with the definitions of the Analytics page: net is
realized P&L less the fee, a trade at exactly zero net is flat (neither a win nor a loss), and the win rate is wins
over all trades. It reads at most 5,000 trades, the newest; truncated is true
when the window held more.
| Field | Meaning |
|---|---|
trades, wins, losses, flat | Counts. |
winRatePct | Wins over trades, as a percentage. null with no trades. |
netPnL, grossPnL, fees, volume | Net, realized before fees, fees paid, and notional traded (both legs). |
profitFactor, profitFactorKind | Gross profit over gross loss when profitFactorKind is ratio. With wins and no losses the kind is no_losses and the factor is null; with neither it is none. |
expectancy | Net per trade. |
avgWin, avgLoss, rewardToRisk | Averages of each side (null when that side is empty) and their ratio. |
best, worst | The best and worst single trade, net. |
avgLeverage | Weighted by notional. |
hold | timedTrades, and avgMinutes, avgWinMinutes, avgLossMinutes. |
streaks | current (positive is wins in a row, negative losses), longestWin, longestLoss. |
bySide | long and short, each with trades and netPnL. |
bySymbol | symbol, trades, wins, netPnL, best first. |
byDay | date (UTC) and netPnL. |
byWeekday | weekday, trades, netPnL, Sunday first. |
byHour | hourUtc (the hour the trade closed), trades, netPnL. Hours with no trade are left out. |
Drawdown, return and the risk ratios are not here: they need the marked-to-market equity curve, which the Analytics page draws.
Payout states
status is one of pending, claimable, paid, rejected or expired. With an accountId, the answer is
that account's payouts plus the profit payouts that drew from all the trader's funded accounts together, whose
accountId is null.
A sandbox key
A sandbox journal key reads the one paper account, whose id is demo; accountId can be left out. list_accounts,
get_trades, get_metrics, get_open_positions and get_rules answer from it (get_rules in the shape of
GET /sandbox/account). The sandbox keeps no fills, funding, daily equity or payouts, so those four answer empty
lists.
Prompts
| Prompt | What it asks the assistant to do |
|---|---|
weekly-review | Review the last seven days against the seven before: result, win rate, profit factor, best and worst trades, where money was made and lost, and how close the account came to its limits. |
worst-habit | Find the single most costly pattern in the last thirty days, show the trades that prove it, and propose one rule. |
pre-session-checklist | State where the account stands before a session: room to the limits, open positions, the current streak, and a largest acceptable loss per trade. |
Each takes an optional accountId.
Call it from code
A tool call is one JSON-RPC request:
curl https://mcp.perpsfund.com \ -H "Authorization: Bearer $PERPSFUND_JOURNAL_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_metrics","arguments":{"accountId":"<id>","from":"2026-10-01"}}}'
The TypeScript SDK wraps the tools as methods:
import { PerpsFundJournal } from "@perpsfund/api"; const journal = new PerpsFundJournal({ apiKey: process.env.PERPSFUND_JOURNAL_KEY! }); const [account] = await journal.accounts();const week = await journal.metrics(account.id, { from: "2026-10-05", to: "2026-10-11" });console.log(week.netPnL, week.winRatePct, week.profitFactor); for await (const trade of journal.allTrades(account.id, { from: "2026-10-01" })) { console.log(trade.symbol, trade.realizedPnL - (trade.fee ?? 0), trade.holdSeconds);}
A refusal throws PerpsFundError, with the code and message above.
Connect an assistant
| Assistant | Setup |
|---|---|
| Claude Desktop | In claude_desktop_config.json, add a server whose command is npx with the arguments -y, mcp-remote, https://mcp.perpsfund.com, --header, Authorization: Bearer <key>. Restart Claude Desktop. |
| Claude.ai | Add a custom connector with the address above and the request header Authorization: Bearer <key>. The request headers field is still being rolled out by Anthropic and may not be on your account. |
| ChatGPT | Not supported yet: its connectors sign in with OAuth and cannot send a key. |
Connect your AI on Analytics writes the Claude Desktop block for you, with a new key in it.