Skip to content

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.

  1. Create a draft (POST /api/v1/drafts) or pick one (GET /api/v1/drafts).
  2. Edit its active script (PATCH /api/v1/drafts/{id}/script, compiled first).
  3. Backtest it (POST /api/v1/backtests with { draftId }, then collect it).
  4. Iterate in the workspace: save variants as named scripts, backtest them by name, keep notes and studies beside them.
  5. 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.

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.

ScopeReaches
allEvery route below, paid from the account’s credits.
draftsThe /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.

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.

RouteDoes
GET /api/v1/draftsYour drafts, newest first: id, name, script, revision, editedBy, venue, trials.
POST /api/v1/draftsCreate 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}/scriptThe 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}/scriptEdit 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.

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.

Statuserror.codeMeaning
409stale_revisionThe draft moved past baseRevision; error.revision is current. Re-read, redo the edit.
422invalid_scriptDid not compile; error.diagnostics has line, column, code, message. Nothing saved.
422edit_not_uniqueAn edits[i].find matched nothing or more than once.
422invalid_requestNeither or both of script / edits, or a malformed body.
404not_foundNo such draft of yours.

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.

One way to backtest, for any length of run:

RouteDoes
POST /api/v1/backtestsQueue 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 script draft first, 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”
RouteDoes
POST /api/v1/evaluations/fastQueue 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/availabilityWhether the gate can run now.
POST /api/v1/evaluations/fullSubmit to the gate: { script } or { draftId, script: { name } }; 202 { jobId }.
GET /api/v1/evaluations/fullYour full evaluations.
GET /api/v1/evaluations/full/{jobId}One, with its verdict when done.
GET /api/v1/marketsThe markets a venue serves (?venue=).
GET /api/v1/candlesRecent candles: ?venue&market&from&to&interval (unix seconds; interval 60, 300, 900, 3600, 14400 or 86400).
POST /api/v1/trials/previewWhat planned more runs would do to the family’s trial count: { script, planned }. Free.
RouteDoes
GET /api/v1/me/creditsThe account’s balance: credits (micro-USD), displayCredits (whole credits), usd.
GET /api/v1/me/credits/statementGrants, the agent’s usage, charges per run, and spend per API key.

Public, no key. The ETag is the engine hash.

RouteReturns
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.

  • A key: 60 requests a minute. An account: 120 a minute across its keys and its browser’s runs. Over that is 429 with retry-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_overloaded and 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 202 with status: "queued" or status: "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.