crypto-comp public API
Version 1.0.0 · /api/v1/openapi.json (OpenAPI 3.1, public, no key required) · mint a key
Compose, validate, save and backtest trading strategies, and read the results.
**Authentication.** A single bearer token: `Authorization: Bearer mcpk_…`. Mint one in the panel at `/settings/api-keys`; it is shown once and cannot be recovered. The token's first segment identifies the credential and the second proves it — D-19's "key and secret" travelling as one value.
**Scopes.** Each key carries a subset of 5 scopes and each endpoint requires exactly one. There is no implication: `:write` does not grant `:read`.
**Rate limits.** Per key AND per account, against the same tier number, in a fixed 60-second window. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a refusal is `429` with `Retry-After`. Because the window is fixed rather than sliding, a caller can send up to twice the limit across a window boundary. That is a documented property, not a defect.
**Money is always a decimal STRING**, never a JSON number — `"10000.00000000"`, not `10000`. Sending a number where a decimal is expected is a `400`. Parse these with a decimal type, not a float.
**Versioning.** Within `v1`, changes are additive only: fields may be added, never removed, renamed or narrowed. A breaking change would be `/api/v2`.
**Not metered against AI credit.** Calls to this API cost nothing from your credit balance; they are bounded by the rate limit alone. Credit is spent by the in-app AI assistant, which is a different surface.
Quickstart
First, mint a key in your panel. It is shown once and cannot be recovered — store it before closing the dialog. Then set two variables and work down this page:
export CRYPTO_COMP_BASE_URL=https://your-installation.example
export CRYPTO_COMP_API_KEY=mcpk_...Every command below uses only these variables plus $STRATEGY_ID and $RUN_ID, which come out of steps 3 and 4. They are executed verbatim by this project's end-to-end suite against a real installation, so a stale example fails a build rather than your afternoon.
1. See what the engine can do
Every indicator and condition, projected from the engine's own registry. Call this before composing a spec.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" $CRYPTO_COMP_BASE_URL/api/v1/catalog2. Check a spec before you save it
Always 200, even for an invalid spec: the body is a verdict, and an invalid one carries structured errors you can fix and re-submit.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" -H "Content-Type: application/json" -d '{"spec":{"version":"0.1","name":"Simple RSI reversion","symbol":"BTCUSDT","base_timeframe":"15m","indicators":{"rsi":{"type":"RSI","inputs":{"length":14,"source":"close"}}},"entry":{"long":{"op":"crosses_below","left":"rsi","right":30}},"exit":{"long":{"op":"crosses_above","left":"rsi","right":70}}}}' $CRYPTO_COMP_BASE_URL/api/v1/specs/validate3. Save it
201 with {"strategy_id"}. The spec is validated again server-side; an invalid one is 422 and nothing is stored.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" -H "Content-Type: application/json" -d '{"name":"RSI reversion","spec":{"version":"0.1","name":"Simple RSI reversion","symbol":"BTCUSDT","base_timeframe":"15m","indicators":{"rsi":{"type":"RSI","inputs":{"length":14,"source":"close"}}},"entry":{"long":{"op":"crosses_below","left":"rsi","right":30}},"exit":{"long":{"op":"crosses_above","left":"rsi","right":70}}}}' $CRYPTO_COMP_BASE_URL/api/v1/strategies4. Launch a backtest
201 with {"run_id"}. Returns immediately — the run is queued, not finished. `initial_balance` is a decimal STRING; a number is a 400.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" -H "Content-Type: application/json" -d '{"strategy_id":"$STRATEGY_ID","spec":{"version":"0.1","name":"Simple RSI reversion","symbol":"BTCUSDT","base_timeframe":"15m","indicators":{"rsi":{"type":"RSI","inputs":{"length":14,"source":"close"}}},"entry":{"long":{"op":"crosses_below","left":"rsi","right":30}},"exit":{"long":{"op":"crosses_above","left":"rsi","right":70}}},"run_config":{"symbol":"BTCUSDT","base_interval":"15m","timeframes":["15m"],"start":"2026-06-15T00:00:00Z","end":"2026-06-17T00:00:00Z","initial_balance":"10000.00000000"}}' $CRYPTO_COMP_BASE_URL/api/v1/runs5. Poll it
`status` moves queued -> running -> succeeded (or failed). A run that has not finished is a STATUS, not an error.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" $CRYPTO_COMP_BASE_URL/api/v1/runs/$RUN_ID6. Read the numbers
The whole performance suite. Every ratio is a decimal STRING; the three counts and `base_timeframe` are not.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" $CRYPTO_COMP_BASE_URL/api/v1/runs/$RUN_ID/metrics7. Read everything
The full stored result: summary, both equity curves, every fill, the signal log, and — for a futures run — the funding ledger, its net total, any liquidations and the execution stamp (market, account type, leverage, margin mode). Large — prefer /metrics when you only need the figures. `result.version` is the reader's branch point: "0.1" predates the metric suite, "0.2" predates futures, "0.3" carries everything. Stored results are immutable and are never recomputed, so treat every field after `signal_log` as optional.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" $CRYPTO_COMP_BASE_URL/api/v1/runs/$RUN_ID/resultsList your strategies
Newest first, specs omitted. Only ever your own.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" $CRYPTO_COMP_BASE_URL/api/v1/strategiesRead one strategy
Including its full spec. Another account's id is 404, never 403.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" $CRYPTO_COMP_BASE_URL/api/v1/strategies/$STRATEGY_IDList your runs
Optionally filtered to one of your strategies. A strategy id that is not yours yields an empty list, not an error.
curl -H "Authorization: Bearer $CRYPTO_COMP_API_KEY" "$CRYPTO_COMP_BASE_URL/api/v1/runs?strategy_id=$STRATEGY_ID"Endpoints
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /api/v1/catalog | catalog:read | List every strategy block the engine supports. |
POST | /api/v1/specs/validate | catalog:read | Deep-validate a strategy spec and return the verdict. |
GET | /api/v1/strategies | strategies:read | List your saved strategies, newest first. |
POST | /api/v1/strategies | strategies:write | Save a new strategy. The spec is validated first. |
GET | /api/v1/strategies/{id} | strategies:read | Read one of your strategies, including its full spec. |
GET | /api/v1/runs | runs:read | List your runs, newest first, each with its summary. |
POST | /api/v1/runs | runs:write | Queue a backtest. Subject to your subscription limits. |
GET | /api/v1/runs/{id} | runs:read | Read one of your runs: status, timings, error and full result. |
GET | /api/v1/runs/{id}/results | runs:read | Read the full stored result of one of your runs. |
GET | /api/v1/runs/{id}/metrics | runs:read | Read just the performance metrics of one of your runs. |
Scopes
A key carries a subset of these, chosen when you mint it. Each endpoint requires exactly one. There is no implication: strategies:write does not grant strategies:read, and a key that needs both must say both. A key missing a scope gets 403 insufficient_scope naming the scope to add.
| Scope | Grants | Endpoints |
|---|---|---|
catalog:read | Read the block catalog and validate specs | GET /api/v1/catalog POST /api/v1/specs/validate |
strategies:read | List and read your saved strategies | GET /api/v1/strategies GET /api/v1/strategies/{id} |
strategies:write | Save new strategies | POST /api/v1/strategies |
runs:read | List and read your runs, their results and their metrics | GET /api/v1/runs GET /api/v1/runs/{id} GET /api/v1/runs/{id}/results GET /api/v1/runs/{id}/metrics |
runs:write | Launch backtests (subject to your subscription limits) | POST /api/v1/runs |
Rate limits
Counted in a fixed 60-second window, against two ceilings on the same number: one per key and one per account. Minting a second key therefore does not raise your quota — the account ceiling is the real one, and a 429's detail says which of the two you hit.
| Tier | Requests per 60s |
|---|---|
free | 60 |
pro | 600 |
team | 600 |
Every response — successes included — carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets), so you can pace yourself before you are ever refused. A refusal adds Retry-After.
The fixed window has a boundary property worth knowing. A fixed window admits up to 2x the limit across a boundary. Documented property, not a defect. If you send your whole allowance in the last instant of one window and again in the first instant of the next, both are served. It is bounded at twice the limit and it is the price of not storing a record of every request you make.
This API is not metered against AI credit. Calls here cost nothing from your credit balance; the rate limit is the only bound. Credit is spent by the in-app AI assistant, which is a different surface.
Errors
Every error body has exactly two keys: {"error": "<code>", "detail": "…"}. Branch on error, which comes from the closed list below; detail is prose for a human and may change wording at any time. Anything machine-readable beyond the code is a header, so it can be added without changing the shape.
| Status | Meaning |
|---|---|
400 | `bad_request` — The request is malformed — a bad uuid, a missing field, a number where a decimal string belongs. |
401 | `unauthorized` — No key, or a key that is unknown, malformed or revoked. All four are the same answer. |
402 | `tier_limit` — Your subscription does not permit this launch (concurrency, data depth or market). Upgrading fixes it; the detail says which limit. |
403 | `insufficient_scope` — Your key does not hold the scope this endpoint needs. The detail names the scope; mint a key that has it. |
404 | `not_found` — No such object — or it is not yours. Deliberately the same answer, so this can never confirm that someone else's id exists. |
422 | `spec_invalid` — The engine rejected the spec. POST /api/v1/specs/validate returns the structured errors. |
429 | `rate_limited` — You are over your tier's request ceiling. Retry-After says when; the detail says whether the KEY or the ACCOUNT tripped. |
500 | `internal` — Something failed on our side. Nothing about it is your request's fault; retrying is reasonable. |
503 | `unavailable` — The engine's validation service is unreachable. Never treated as 'valid' — a spec is not stored on a guess. |
The full code list: bad_request unauthorized tier_limit insufficient_scope not_found spec_invalid rate_limited internal unavailable
Two are worth a note. 404 also means "not yours" — deliberately the same answer as "does not exist", so this API can never confirm that another account's id is real. 422 does not carry the validator's errors , because the body is two keys — send the spec to POST /api/v1/specs/validate, which answers 200 with the structured errors, fix them, and try again.
Money
Every monetary and statistical value crosses this API as a decimal string — "10000.00000000", never 10000. Parse them with a decimal type, not a float: 0.00000001 round-trips through a double as 1e-8, which this API will reject on the way back in. Sending a JSON number where a decimal string belongs is a 400.
The exceptions are the fields that are genuinely counts, not amounts: trade_count, bar_count, max_drawdown_duration_bars and the base_timeframe label.
Versioning
Within /api/v1, changes are additive only. Fields may be added to a response; none will be removed, renamed, or have its type narrowed, and no endpoint will move. A change that would break a working client ships as /api/v2, alongside this one.
So: ignore fields you do not recognise rather than failing on them, and branch on the error code rather than on detail or on a status alone.
Paths outside /api/v1 — everything under /api/ that this page does not list — are internal to the web application, are not versioned, and change without notice. Please do not build against them.
Keys
A key looks like mcpk_<12 hex>_<secret>. The middle segment identifies which credential you presented and proves nothing; the last segment is the secret. They travel together as one bearer token, so there is nothing to pair up.
We store only a hash, so the key is shown exactly once and we cannot recover it for you — a lost key is revoked and replaced. Revocation takes effect immediately on the next request. The mcpk_ prefix exists so a leaked key is findable by a secret scanner: if one lands in a public repository, revoke it in the panel.
Variables used by the examples on this page: $CRYPTO_COMP_BASE_URL $CRYPTO_COMP_API_KEY $STRATEGY_ID $RUN_ID