Build on the engine.
A JSON API, a live stream, signed webhooks and an MCP connector for the same engine documents you see in the Workspace, projected to your plan.
What you can build
Dashboards
Pull the dashboard and calendar documents into your own screen, with ETag caching or the live stream.
Bots
Receive signed alert deliveries at your own https endpoint and route them to chat, a queue or a spreadsheet.
AI assistants
Connect an MCP client so your assistant can read the calendar, the regime and the Academy while you work.
Quickstart
https://priceisall.com/api/v1Sign in with POST /auth/login and send the access token as Authorization: Bearer .... Refresh with POST /auth/refresh. The keys on your Workspace keys page are for AI models, not for this API.
Success is { "data": ... }. Failure is { "error": { "code", "message" } } with a stable code.
curl
# 1. Sign in
curl -s https://priceisall.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"..."}'
# Copy data.tokens.access_token from the reply.
# Refresh later with:
# curl -s https://priceisall.com/api/v1/auth/refresh -H "Content-Type: application/json" \
# -d '{"refresh_token":"..."}'
# 2. Read the dashboard for your plan
curl -s https://priceisall.com/api/v1/engine/dashboard \
-H "Authorization: Bearer $ACCESS_TOKEN"JavaScript
const BASE = "https://priceisall.com/api/v1";
const login = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "[email protected]", password: "..." }),
}).then((r) => r.json());
const token = login.data.tokens.access_token;
const res = await fetch(`${BASE}/engine/dashboard`, {
headers: { Authorization: `Bearer ${token}` },
});
const { data } = await res.json(); // { error: { code, message } } on failurePython
import requests
BASE = "https://priceisall.com/api/v1"
login = requests.post(f"{BASE}/auth/login",
json={"email": "[email protected]", "password": "..."})
token = login.json()["data"]["tokens"]["access_token"]
res = requests.get(f"{BASE}/engine/dashboard",
headers={"Authorization": f"Bearer {token}"})
data = res.json()["data"] # {"error": {...}} on failureEndpoint reference
Sign in
Sessions are a short-lived access token plus a refresh token.
| Method | Path | Auth | What it does |
|---|---|---|---|
| POST | /auth/login | None | Exchange email and password for an access token and a refresh token. |
| POST | /auth/refresh | Refresh token | Rotate a session. Send {"refresh_token": "..."} as JSON. Returns a new token pair. |
| POST | /auth/logout | Refresh token | End a session. |
| GET | /me | Bearer | Your account profile. |
Engine documents
The same documents the Workspace renders, projected to your plan.
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /engine | Bearer | Manifest: version, publish time, your entitlements, and an ETag per document as you see it. |
| GET | /engine/{key} | Bearer | One document, projected to your plan. Keys: dashboard, calendar, calendar_history. Supports If-None-Match. |
| GET | /engine/stream | Bearer | Server-sent events: the same documents, pushed when they change. |
| GET | /public/engine/{key} | None | The official calendar, signed out (calendar and calendar_history only). Mounted only when the public calendar is switched on. |
Alerts
Follows, channels and the inbox behind the Alerts page.
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /alerts/follows | Bearer | The markets you follow. |
| PUT | /alerts/follows | Bearer | Replace the markets you follow. |
| GET | /alerts/channels | Bearer | Channel states: in-app and webhook. |
| PUT | /alerts/channels/{channel} | Bearer | Turn a channel on or off. |
| GET | /alerts/inbox | Bearer | In-app alerts. Pass ?after=<seq> to page forward. |
| GET | /alerts/deliveries | Bearer | Delivery history, metadata only. |
Webhook endpoints
Manage where signed alert deliveries go. Up to 3 endpoints per account.
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /alerts/webhooks | Bearer | List your endpoints. A secret is never returned here. (Maestro) |
| POST | /alerts/webhooks | Bearer | Create an endpoint from {"url": "https://..."}. The reply carries the signing secret once. (Maestro) |
| PUT | /alerts/webhooks/{id} | Bearer | Pause or resume an endpoint with {"enabled": true}. (Maestro) |
| DELETE | /alerts/webhooks/{id} | Bearer | Delete an endpoint and its secret. (Maestro) |
| POST | /alerts/webhooks/{id}/rotate | Bearer | Replace the secret at once. The new one is shown once. (Maestro) |
| POST | /alerts/webhooks/{id}/test | Bearer | Send a signed ping and report the outcome. (Maestro) |
Catalog
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /plans | None | The public plan catalog. |
Document reads return an ETag and answer 304 to If-None-Match. Headers X-Readmodel-Version and, when the data is old, X-Readmodel-Stale: 1 come with them.
Live stream
GET /engine/stream keeps one connection open and sends a frame whenever your projection changes. Resume with the Last-Event-ID header or the last_event_id query parameter.
event: docA document changed for your projection. Data is the document envelope.
event: goneA document you had was removed. Data: key and version.
event: alertAn in-app alert, when alerts are on. Carries no id line.
event: byeThe server is closing. Reasons: shutdown, max_age, unauthorized, unavailable, entitlements_changed.
Webhooks
Alerts can post to your own https endpoint. Create one with the API or on the Alerts page, keep the secret it shows once, and verify every request before you trust it.
Plan
Webhook alerts need Maestro (engine.reads).
Endpoint rules
https only, port 443, a public host name (no IP addresses), at most 2048 characters. Up to 3 endpoints.
Headers
PriceIsAll-Signature, PriceIsAll-Delivery and Idempotency-Key. User agent: PriceIsAll-Webhooks/1.
Signature
t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>." followed by the raw body, keyed with your secret>. Reject a timestamp more than 300 seconds old.
Retries
A non-2xx answer or no answer is retried after 10 s, 30 s, 2 m, 10 m, 30 m and 1 h: 7 attempts in all. Each attempt times out after 5 seconds.
Auto-off
After 5 dead deliveries in a row the endpoint switches off. Send a successful test to turn it back on.
Deduplicate
Use delivery_id or the Idempotency-Key header. A retry carries the same ids and a fresh signature timestamp.
Create an endpoint
curl -s https://priceisall.com/api/v1/alerts/webhooks \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/priceisall"}'
# The reply includes the signing secret once. Then send a test:
# curl -s -X POST https://priceisall.com/api/v1/alerts/webhooks/$ID/test \
# -H "Authorization: Bearer $ACCESS_TOKEN"Test ping body
{
"api_version": "pia-webhook/1",
"delivery_id": "ping_0f3a9c2e...",
"sent_at": "2026-10-17T09:30:00Z",
"type": "ping"
}Alert body
{
"api_version": "pia-webhook/1",
"data": { ... },
"delivery_id": "...",
"read_id": "...",
"recorded_at": "2026-10-17T09:30:00.123Z",
"type": "alert.post_read"
}data is the same alert content you see in your Inbox. Keys are sorted. The body is signed byte for byte, so verify the raw body before parsing it.
Verify a delivery
JavaScript (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body as a string, exactly as received.
export function verify(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const got = Buffer.from(parts.v1 ?? "", "hex");
const exp = Buffer.from(want, "hex");
return got.length === exp.length && timingSafeEqual(got, exp);
}
// Read the header from "PriceIsAll-Signature".Python
import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if not t or abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
want = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))
# Read the header from "PriceIsAll-Signature".MCP for AI assistants
A read-only MCP server for assistants that support remote MCP connectors, such as Claude or ChatGPT style clients. The assistant signs you in through OAuth, you approve what it may read on a consent screen, and you can revoke it from your account at any time. It has no tool that places a trade or spends your balance.
https://mcp.priceisall.com/mcp- Open your assistant's settings and add a custom connector or remote MCP server.
- Paste the server URL below. No API key is needed.
- Your assistant opens a PriceIsAll sign-in. Approve what it may read on the consent screen.
- Ask a question such as "What does the official calendar show for this week?"
- Revoke the app any time from Connected apps in your account.
| Tool | Reads | Plan |
|---|---|---|
| get_regime | The market regime as your plan sees it. | All plans |
| get_track_record | Published Ledger entries. | All plans |
| verify | Check a Ledger entry or batch by id. | All plans |
| get_calendar | The official calendar for a window of up to 21 days. | All plans |
| get_release_history | History of one scheduled release. | All plans |
| get_news | Official news headlines. | All plans |
| search_academy | Search Academy lessons. | All plans |
| get_lesson | One Academy lesson. | All plans |
| get_glossary | One glossary term. | All plans |
| search_academy_sections | Search bounded Academy sections. | All plans |
| get_academy_section | One Academy section by id. | All plans |
| get_market | One market from the board. | Maestro |
| get_market_map | The market map, by asset class and horizon. | Maestro |
| explain | Explain one Ledger call by id. | Maestro |
For client authors
| /mcp | The MCP endpoint your client connects to. |
| /.well-known/oauth-protected-resource | OAuth resource metadata. Clients read this first. |
| /.well-known/oauth-authorization-server | OAuth server metadata: authorize, token and revoke endpoints. |
| /oauth/authorize | Where you sign in and approve the app. |
| /oauth/token | Token exchange and refresh. |
| /oauth/revoke | Token revocation. |
Desktop agents
Claude Desktop, Claude Code and Cursor reach the same MCP connector directly. Nothing else needs to be installed. The connector is read-only for every client. It has no tool that places a trade, spends a balance or changes your account, and you can revoke any app from Connections.
Claude Desktop
- Open Settings, then Connectors, and choose to add a custom connector.
- Paste https://mcp.priceisall.com/mcp as the server URL. Leave any advanced client fields empty.
- Claude opens a PriceIsAll sign-in in your browser. Approve the read-only access on the consent screen.
Claude connects through its own connector service, which is why no client id or secret is asked for here.
Claude Code
- Add the server with the pre-registered client and a fixed callback port. The server has no dynamic registration, so the client id is required.
- Run /mcp inside Claude Code, or claude mcp login priceisall in your shell, to open the browser sign-in.
- Approve the read-only access on the consent screen.
claude mcp add --transport http \
--client-id pia-cursor --callback-port 8080 \
priceisall https://mcp.priceisall.com/mcpThe client is public, so no secret is set. Any callback port works because loopback redirects ignore the port.
Cursor
- Open ~/.cursor/mcp.json (or the project's .cursor/mcp.json) and add the server below.
- Cursor shows the server as needing sign-in. Choose connect and approve the read-only access in your browser.
{
"mcpServers": {
"priceisall": {
"url": "https://mcp.priceisall.com/mcp",
"auth": { "CLIENT_ID": "pia-cursor" }
}
}
}Cursor's desktop app returns to http://localhost:8787/callback, which the pia-cursor client allows.
What each plan sees
| Apprentice | 11 tools: regime, track record, calendar, news and the Academy. |
| Maestro | All 14 tools, adding market, market map and explain. |
What each plan includes
| Capability | Apprentice | Maestro |
|---|---|---|
| Dashboard headline and official calendar (API and stream) | Yes | Yes |
| Regime composite and multiplier | No | Yes |
| Board documents: pairs, assets, overlap, coverage | No | Yes |
| Webhook alert endpoints | No | Yes |
| MCP: calendar, regime, Academy, news, track record tools | Yes | Yes |
| MCP: market, market map and explain tools | No | Yes |
Apprentice
$0engine.headlinegenerated_at, source and regime.state on the dashboard, plus the official calendar.
Maestro
$39 / monthengine.headlineengine.regimeengine.readsengine.boardEverything in Apprentice, plus regime.composite and regime.multiplier, and the board: pairs, assets, asset_overlap and coverage. Post-Read items arrive through alerts, not as an /engine document.
The server only removes fields. It never rounds, reorders or recomputes a value, and a field outside your plan is absent, not null. Your own entitlements are listed in the GET /engine manifest.
Limits
Streams
Defaults (ENGINE_STREAM_PER_USER, ENGINE_STREAM_MAX): 4 open streams per account, 1000 per server. A stream ends after about 15 minutes with a bye event; reconnect with last_event_id to resume.
Sign in
5 requests per minute per address, burst 5. Login is also limited per email.
Refresh
60 per minute per address, burst 20 (the default).
Public calendar
60 per minute per address, burst 30 (the default).
MCP connector
60 requests per minute per connected app, burst 20 (the default, MCP_RATE_PER_MIN and MCP_RATE_BURST). Access tokens last 60 minutes and refresh tokens 30 days by default.
Webhook endpoints
3 endpoints per account. A test ping is limited to 3 at once, then 1 per minute.
Document reads
No per-request limit is defined in code. Use ETag and If-None-Match, or the stream, instead of polling.
Errors
{
"error": {
"code": "unauthorized",
"message": "Authentication is required."
}
}| 400 | bad_request | Malformed request body (login, refresh). |
| 401 | unauthorized | No valid Bearer token, or bad credentials at login ("Invalid email or password."), or an invalid or expired refresh token. |
| 403 | forbidden | Login for a suspended account. |
| 422 | validation_failed | A field failed validation, for example a missing refresh_token. |
| 404 | not_found | The key is not a customer document. |
| 404 | engine_doc_missing | The current version has no such document. |
| 429 | rate_limited | Too many requests from one address, or too many sign-in attempts for one email. |
| 429 | engine_stream_limit | Too many open streams for the account or the server. |
| 403 | not_entitled | Webhook endpoints need Maestro. |
| 409 | webhook_limit | You already have 3 webhook endpoints. |
| 409 | webhook_needs_test | An endpoint switched off after failures. Send a successful test to turn it back on. |
| 409 | webhook_secret_unreadable | Rotate the secret, then send a test. |
| 503 | webhook_busy | Too many test pings are running. Try again in a few seconds. |
| 503 | engine_unavailable | No read-model version has been published yet. |
| 503 | engine_not_configured | The read API is not switched on for this server. |
Embeddable widgets
No code needed for the public data. Pick a widget, preview it and copy the embed snippet from the widgets gallery. Embeds carry only data that is already public and a link back to PriceIsAll.
Start from the Workspace.
A free account reads the headline and the official calendar. Manage AI keys and connected apps from your Workspace.