Backtest workspace API
The Backtest workspace API is the Build page’s surface over HTTP: drafts and the draft’s active script, its workspace (named, versioned scripts with their runs and evaluations, studies, notes), proposals, backtests, fast and full evaluations, markets and candles, and credits. An external agent (Claude Code, Codex or your own) works on the same drafts, runs the same backtests and evaluations, and spends the same credits as you do in the app; the agent skill is the workflow. The builder agent inside the app calls exactly these routes for its user, so the two reach identical results.
Workflow
Section titled “Workflow”- Create a draft (
POST /api/v1/drafts) or pick one (GET /api/v1/drafts). - Edit its active script (
PATCH /api/v1/drafts/{id}/script, compiled first). - Backtest it (
POST /api/v1/backtestswith{ draftId }, then collect it). - Iterate in the workspace: save variants as named scripts, backtest them by name, keep notes and studies beside them.
- Check a promising script with a fast evaluation (advisory), then iterate with full evaluations (the gate). Each verdict attaches to the version it evaluated.
Base URL https://mainnet.tradepnl.xyz (testnet https://testnet.tradepnl.xyz),
the API’s own hostname. The app hostnames serve the browser app only.
Authentication
Section titled “Authentication”Every route takes either the signed-in browser session or an API key of the
same account, Authorization: Bearer pnl_sk_…. Nothing is anonymous: without
either, a route answers 401. (The builder agent inside the app authenticates
as a registered worker with a short-lived capability for one turn of one
conversation; it holds no key and no password.) Create keys under Agent CLI
in the editor’s agent chat menu (at most three per account); a key acts as its account, and what it runs
is attributed to it (key:<label>) in the draft’s activity and the credit
statement.
| Scope | Reaches |
|---|---|
all | Every route below, paid from the account’s credits. |
drafts | The /api/v1/drafts routes only: read and edit, no compute. Any other route answers 403 forbidden_scope. |
A key reaches nothing else: not trading, allocation, venue keys, buying credits or managing keys. Listing an algo stays with the signed-in wallet.
Credits
Section titled “Credits”Backtests, fast evaluations and full evaluations cost credits at the live price
book (GET /api/v1/prices, public, whole credits of $0.01). The price is
reserved before the run starts, charged when it completes and released when it
fails, so a failed run costs nothing. A balance that cannot cover the price is
refused before anything runs, on every paid route, with one body:
HTTP 402{ "error": "insufficient_credits", "message": "This needs 5 credits; the account has 2.", "required": 5, "balance": 2, "topUp": "/credits" }required and balance are whole credits; topUp is where the account buys
more.
Routes
Section titled “Routes”Drafts and the active script
Section titled “Drafts and the active script”| Route | Does |
|---|---|
GET /api/v1/drafts | Your drafts, newest first: id, name, script, revision, editedBy, venue, trials. |
POST /api/v1/drafts | Create one: { name, script, venue }. At most three per account. |
GET /api/v1/drafts/{id} | One draft. |
PUT /api/v1/drafts/{id} | Save the whole draft: { name, script, baseRevision }; may hold a program that does not compile. |
DELETE /api/v1/drafts/{id} | Remove it. |
GET /api/v1/drafts/{id}/script | The active script, the one the editor shows: { script, revision, markets, diagnostics, name, venue, editedBy, trials }. markets is null while it does not compile. |
PATCH /api/v1/drafts/{id}/script | Edit and save: { baseRevision, script } or { baseRevision, edits: [{ find, replace }] }. Compiled first. |
Every saved edit appears live in the owner’s open editor, attributed to the key.
PATCH /api/v1/drafts/{id}/script
Section titled “PATCH /api/v1/drafts/{id}/script”edits are applied in order, and each find must match exactly once in the
current program. The result is compiled before anything is saved.
{ "baseRevision": 3, "edits": [{ "find": "emit BTC, 0.0", "replace": "emit BTC, 0.5" }] }Saved: 200 { saved: true, revision, markets, holds, fuel: { worstCase, budget }, warnings }.
fuel.worstCase is the compiler’s proven worst case for one bar; fuel.budget is the
language’s per-bar cap (2,000,000), which the worst case must fit. A script declares no fuel.
| Status | error.code | Meaning |
|---|---|---|
| 409 | stale_revision | The draft moved past baseRevision; error.revision is current. Re-read, redo the edit. |
| 422 | invalid_script | Did not compile; error.diagnostics has line, column, code, message. Nothing saved. |
| 422 | edit_not_unique | An edits[i].find matched nothing or more than once. |
| 422 | invalid_request | Neither or both of script / edits, or a malformed body. |
| 404 | not_found | No such draft of yours. |
Workspace and proposals
Section titled “Workspace and proposals”Under /api/v1/drafts/{id}/workspace/: status, activity; scripts
(?passed=1), scripts/{name} (?sha=), PUT scripts/{name} ({ source, baseSha?, requireCompile? }: requireCompile refuses a program that does not
compile), PATCH scripts/{name} ({ edits, baseSha, requireCompile? }, a new
version), DELETE scripts/{name}, scripts/{name}/result,
scripts/{name}/evaluation?kind=fast|full; notes, notes/{name} (PUT with
{ content, baseRevision }, PATCH with { edits, revision }, DELETE);
studies, studies/{id} (PUT with { study, meta, revision, trials, trialsRevision }, DELETE). The builder agent’s proposals:
GET /api/v1/drafts/{id}/proposals, POST …/proposals/{pid}/accept and
POST …/proposals/{pid}/reject ({ comment }); the builder agent itself makes
them (POST /api/v1/drafts/{id}/proposals), never a key.
Backtests
Section titled “Backtests”One way to backtest, for any length of run:
| Route | Does |
|---|---|
POST /api/v1/backtests | Queue a run; 202 { id, status: "queued" }. |
GET /api/v1/backtests/{id} | Wait up to a minute (?waitMs=) for it: 202 while it runs, then the result, once. ?detail=summary for the numbers only; ?store=true keeps the full result on the workspace script. |
What runs is one of:
{ "draftId": "…" }: the draft’s own program on the draft’s venue. It is saved as the workspace scriptdraftfirst, and the result attaches to it.{ "draftId": "…", "script": { "name": "…" } }: a script of the draft’s workspace, its current version.{ "script": { "source": "…", "name": "…" }, "venue": "…" }: any program.
The result is a simulation on public history, net of platform fees, venue base-tier fees,
funding and modelled slippage: metrics, summary, trades, fee, the equity
curve and per-market daily attribution (detail=full), and trialsUsed.
The platform fee is included without
referral discounts. execution.platformFeeBps is the 5 bps cap;
fee.platformFeeBps is the realized average rate and fee.paid is the sum of
platform charges on simulated fills.
Every completed run is a trial of its program’s family for the account.
Evaluations, markets and the trial preview
Section titled “Evaluations, markets and the trial preview”| Route | Does |
|---|---|
POST /api/v1/evaluations/fast | Queue the advisory tradeability measurement: { script, venue }, or { draftId, script: { name } } for a workspace script, whose summary then attaches to it; 202 { id }. |
GET /api/v1/evaluations/fast/{id} | The result, once: waits up to waitMs (at most 60 s) and answers 202 while it runs (?detail=summary|full). Charged when collected; a failed run costs nothing. |
GET /api/v1/evaluations/full/availability | Whether the gate can run now. |
POST /api/v1/evaluations/full | Submit to the gate: { script } or { draftId, script: { name } }; 202 { jobId }. |
GET /api/v1/evaluations/full | Your full evaluations. |
GET /api/v1/evaluations/full/{jobId} | One, with its verdict when done. |
GET /api/v1/markets | The markets a venue serves (?venue=). |
GET /api/v1/candles | Recent candles: ?venue&market&from&to&interval (unix seconds; interval 60, 300, 900, 3600, 14400 or 86400). |
POST /api/v1/trials/preview | What planned more runs would do to the family’s trial count: { script, planned }. Free. |
Credits
Section titled “Credits”| Route | Does |
|---|---|
GET /api/v1/me/credits | The account’s balance: credits (micro-USD), displayCredits (whole credits), usd. |
GET /api/v1/me/credits/statement | Grants, the agent’s usage, charges per run, and spend per API key. |
Script reference
Section titled “Script reference”Public, no key. The ETag is the engine hash.
| Route | Returns |
|---|---|
GET /api/v1/reference/script | { langVersion, engineHash, sections: [{ id, title }] } |
GET /api/v1/reference/script/{section} | That section as markdown, with samples. |
The same reference is the Script language page.
Limits
Section titled “Limits”- A key: 60 requests a minute. An account: 120 a minute across its keys and its
browser’s runs. Over that is
429withretry-after. - At most two backtests of an account queued or running at once
(
429 too_many_backtests). Finished or failed jobs free slots without collection. - The network backtest queue holds at most 250 queued or running jobs. Excess
submissions fail immediately with
503 queue_overloadedand retry guidance. - A fast backtest waits at most 60 seconds for its first worker (
queue_timeout), then has 30 seconds for loading and execution (execution_timeout). Retries share that execution budget. Both timeouts are HTTP 503 and cost no credits. - Collection returns
202withstatus: "queued"orstatus: "running"while pending. Its separate 60-second HTTP wait can end before the job does: poll the same job ID again. Do not resubmit an ambiguously accepted POST. - A script is at most 64,000 characters; one PATCH carries at most 50 edits.