sentinelHow it worksPricingBlogSign in

Start

IntroductionQuickstartAuthentication

Guides

Monitor a contractAdmin changesLarge outflowssBTC and PoX-5Slack and webhooks

Concepts

Watches & alertsBilling

Reference

CLIMCP serverSDKAPI reference
Powered bysecondlayer
Reference / API reference
Previous← SDK

On this page

API reference

The HTTP API for Stacks contract monitoring: read plans and alerts, and change monitoring with the same routes the app uses.

Base URL: https://api.runsentinel.app

Send Authorization: Bearer <token>. The token is an sk-snt_… key or an ss-sl_… login session; see Authentication. A secondlayer API key won't work here; Sentinel keys start sk-snt_. Every /v1 response carries Sentinel-API-Version: 1.

shell
curl https://api.runsentinel.app/v1/me \
  -H "Authorization: Bearer $SENTINEL_API_KEY"

Errors are { "error": string } with 401 (no or bad token), 403 (not allowed for this key), 404, 429 or 503 (sign-in unavailable, try again). The limit is 120 requests a minute per key.

A secondlayer session's first request creates your Sentinel account from the email secondlayer verified. A secondlayer account Sentinel's sign-in just created is linked at once. An email that already had a secondlayer account and hasn't approved Sentinel gets 403 This email already has a secondlayer account. Allow Sentinel to use it: run sentinel login --force and approve in the browser..

Stability

Within v1, fields are only added, never renamed or removed. A breaking change ships as /v2. Ignore fields you don't know.

Changelog

2026-10-28, plan runs drop kind. Plan runs no longer return kind (it was always "audit"). Removed, no replacement.

2026-10-28, a plan's menu is contract. report.watchable on GET /v1/plans/:id and a plan's views, and watchable on POST /plan/preview and the teaser, are now contract (same object, no alias). Stored plans moved with it.

2026-10-27, POST /plan/:id/monitor takes { deployer } only. The watches field is gone (an extra field is ignored): turn watches on with POST /v1/watches/:id/state or POST /v1/watches, then save. A plan's report no longer carries functions.

2026-10-27, plan watch routes removed. Removed (no replacement alias): GET /plan/:id/watches, POST /plan/:id/watches, POST /plan/:id/watches/:watchId/rule, GET /plan/:id/watches/:intentId/activity, GET /plan/:id/functions, GET /plan/:id/functions/:fn/activity and GET/POST /plan/:id/watches/asset/:asset/threshold. Write a watch with POST /v1/watches (a limit is --over on its line), read it with GET /v1/watches and GET /v1/watches/:id, its events with GET /v1/logs, and a contract's surface with GET /v1/plans/:id. POST /plan/:id/monitor stays.

2026-10-27, one watch model. Removed (no replacement alias): POST /plan/:id/watches/state, POST /plan/:id/watches/:watchId/delivery (turn a watch on or off, mute it and note it with POST /v1/watches/:id/state and POST /v1/watches/:id/delivery), POST /contracts/watch (a first POST /v1/watches adds the contract), the intent= parameter of GET /v1/alerts, and planId from a watch (GET /v1/watches, POST /v1/watches and its preview). A watch's check can no longer be set to null through the API.

2026-10-27, projects removed. An account watches contracts directly. Removed (no replacement alias): every /projects route, projects on GET /v1/me, ?project= on /v1/watches and /v1/logs, project targets for schedules and the prj_… id, the project field of a watch's adds and added. Added: GET /v1/contracts, and the same rows as contracts on GET /v1/me. Changed: the import routes are POST /contracts/import/preview and POST /contracts/import (no project, hold per contract; a repo contract's state is added where it was in-project); a schedule with no target is a digest of every contract you have live, and a digest's target is { kind: "contract", id, name } or { kind: "account" }; POST /plan/:id/remove also takes a contract you only watch.

2026-10-24, minted. Added: the intent kind minted, the subject kind mint ({ kind: "mint", asset }: the row's contractId is the token's contract), the actor signal newMinter, and the trigger when mint <token-contract> <token-name> [--new-minter | --over N]. A Minted alert's facts are Amount, To and Minted through; its signal is { kind: "newMinter", minter, amount }.

2026-10-20, alert links. Removed (no replacement alias): links.alert on alerts (API, webhook, SDK types); alert emails and Slack messages no longer link to a web page. Each alert ends with the command to inspect it.

2026-10-19, cleanup. Removed (no replacement alias, nothing held for old clients): GET /me, GET /alerts and GET /plan/:id (use GET /v1/me, GET /v1/alerts and GET /v1/plans/:id); the ?kinds=all parameter of GET /plan/:id/watches (it always lists every kind); a watch choice named by its function alone (each choice in POST /plan/:id/monitor needs watchId, else 400); POST /keys { name, actions } (400; send areas) and the old read / read,actions key scopes; local snt_ keys (401) and the legacy flag on GET /keys; the watch field pendingRule and the status pending-review; stats7d on intent watches (read the last 7 of stats30d). GET /v1/me now also opens the account on secondlayer, as /me did. A watch's rule kind system is listed in the types.

2026-10-19, quiet Money out and reorgs. Changed: a new Money out turned on with no threshold (a token, not any token or an NFT) alerts on a window sum, 1.5 times the largest 720-block sum on record, once 20 outflows are on record; every outflow until then. Rows already live keep their rule. Added: an alert whose event a reorg took off chain gets one alert.updated (reason: "reorg"), only once its transaction hasn't come back for 10 minutes and secondlayer's Index confirms it isn't onchain.

2026-10-19, urgent alerts and checks. Changed: a check no longer holds back an urgent alert. It goes at once, saying the check is running, and the verdict follows as an update to it. Added: the webhook event alert.updated and the check status no.

2026-10-19, status. Added: GET /v1/status (is monitoring up, measured, with block-to-alert latency p50 and p95 over 24 h and 7 d; open without a token, and with one it adds your own subscriptions), POST /v1/status/watch (a monitoring-is-up line in your digests), the digest field monitoring and the webhook event monitoring.degraded. /health now reports real state and answers 503 when any part is past its threshold.

2026-10-19, delivery gaps. Added: the alert field late (Sentinel received the event more than 10 minutes after its block: a replay after a delivery gap, or an event recovered from history; its summary says when it happened) and the webhook event monitoring.recovered (delivery of a contract's events stopped and is back, with the blocks replayed).

2026-10-18, watched contracts. Added: a project contract's origin can be watched, a contract you monitor without a claim (planId is the plan your monitoring opened on); POST /contracts/watch puts it in a project, and stopping its monitoring takes it out. Removed: watching on GET /v1/me (never listed here): a contract you only monitor is now a project member.

2026-10-18, checks. Added: a watch's check (a model answers your question on each event first; it alerts only on a yes whose reasons cite what it was shown), the alert field check, confirm on POST /v1/watches and the watch delivery route, POST /v1/checks/backtest, GET /v1/checks and GET /v1/checks/:id, a schedule's check and a digest's ask, the digest fields checks and ask, and the alert kind check.

2026-10-18, Slack app. Added: POST /integrations/slack/install, GET /integrations/slack, DELETE /integrations/slack/:team (connect the Sentinel Slack app) and #channel as a Slack destination target.

2026-10-18, alert links. Added: the alert field inspect (the commands that show more of an alert) and signed one-click Mute 1 h / Mark read links in email and Slack. The webhook carries no link.

2026-10-18, schedules. Added: /v1/schedules (a digest of a contract or project on a clock: every, a cron line or at), POST /v1/schedules/preview, and the webhook event digest.created. A digest is free.

2026-10-18, triggers. Added: POST /v1/watches and POST /v1/watches/preview (a trigger spec in, a watch out), /v1/destinations (named destinations), the subject kinds inflow, asset with nft: true and group admin / value, a watch's match and delivery.to, the intent kinds money_in, admin, value, and spec / line on every listed watch. GET /plan/:id/watches leaves the new kinds out unless ?kinds=all.

2026-10-18, 0.10.0. A one-time exception to "fields are only added": no external consumer read these. Removed: the alert fields findingKind, watchSubject, possibleApproachInFinding and origin; the alert kind finding (a stored one reads null); GET /v1/plans/:id/findings/:n and GET /v1/plans/:id/runs; a plan's findings, verdict fields, verifierReport and counts; a claim's evidence and a project's findings; the watches' locked; the rule kinds signature and audit. A plan's access is claimed, or absent on the public teaser.

Reads

All GET.

GET /v1/me

You, your claims, every contract you have (added or only watched), plans being made, and unread alerts.

json
{
  "user": { "id": "usr_…", "email": "dev@example.com", "createdAt": "…" },
  "claims": [],
  "contracts": [],
  "unreadAlerts": 2,
  "latestAlerts": [],
  "liveBlocked": null
}

liveBlocked is null once the account can turn monitoring on. Until then it is { "reason": "consent" | "setting_up", "message" }, and every write that would turn a watch on gets 403 with that message. consent: allow Sentinel to use your secondlayer account (sign in at runsentinel.app, or sentinel login). setting_up: the account is still being set up; try again in a minute. GET /billing carries the same field.

A write that would put a contract live past your tier's limit gets 402 with { "error", "code": "tier_limit", "tier", "used", "limit", "upgradeUrl" }: the error names the contracts in use and how to turn one off. A contract whose code is already live (the same source on another contract) passes. code: "tier_required" means the tier doesn't include it at all (when sbtc or when pox5 on Free, a live check on Free); code: "payment_paused", a check while your payment is failing out. GET /billing returns tier (id, name, founding, status: active, grace, paused or cancelling, periodEnd, cancelAt, source, and liveCodes: used, limit and the contracts grouped by code), checks (periodRuns, periodUnits, periodUsd, pricePerRunUsd, deepPricePerRunUsd), monthEvents (counted, never billed), manageUrl and cutover (the day this account's live mainnet watches turn off if it still holds no tier, once it has been told; else null).

GET /v1/status

Is monitoring up, measured. Anyone may read it without a token: it holds counts, no contract and no account. With a token it adds account: the subscriptions behind your live watches, whether your monitoring is degraded and whether your digests carry the monitoring line (POST /v1/status/watch {"on": true}).

json
{
  "ok": true,
  "checkedAt": "…",
  "db": { "ok": true },
  "subscriptions": { "ok": true, "total": 12, "degraded": 0, "circuitsOpen": 0, "lastDeliveryAt": "…", "checkedAt": "…" },
  "outbox": { "ok": true, "pending": 0, "oldestSec": null, "alertsPending": 0, "alertsOldestSec": null },
  "lanes": { "ok": true, "lanes": [{ "lane": "schedules", "ageSec": 12, "everySec": 30, "ok": true }] },
  "latency": {
    "24h": {
      "alerts": 41, "deliveries": 63, "late": 0,
      "blockToDelivered": { "p50Sec": 0, "p95Sec": 0 },
      "blockToReceived": { "p50Sec": 0, "p95Sec": 0 },
      "receivedToJudged": { "p50Sec": 0, "p95Sec": 0 },
      "judgedToDelivered": { "p50Sec": 0, "p95Sec": 0 },
      "byChannel": { "email": { "p50Sec": 0, "p95Sec": 0, "deliveries": 41 } }
    },
    "7d": { "…": "the same, over 7 days" }
  }
}

latency is measured on every alert the webhook lane raised: when its block was made, when Sentinel received the event, when it judged it, and when each channel took it. p50 and p95 by nearest rank; an alert received more than 10 minutes after its block (a replay or a recovered event) is counted in late, not in the percentiles. The zeros above stand in for live numbers.

Each part has a threshold: a write must go through; no subscription degraded (its events haven't reached Sentinel for 10 minutes while the chain moved on); nothing taken and left undispatched past 5 minutes, no alert unsent past 30; every lane ticked within three of its intervals. ok is all of them. GET /health answers the same report, 503 when ok is false.

GET /v1/plans

One entry per contract you claimed.

json
{
  "plans": [
    {
      "contractId": "SP….example-vault",
      "planId": "pln_example",
      "createdAt": "…",
      "access": "claimed",
      "status": "done",
      "watches": 4,
      "monitoring": "live",
      "finishedAt": "…"
    }
  ]
}

GET /v1/plans/:id

The plan, at your access level. A claimant's view, trimmed:

json
{
  "view": "full",
  "access": "claimed",
  "id": "pln_example",
  "contractId": "SP….example-vault",
  "tier": "facts",
  "status": "done",
  "startedAt": "…",
  "finishedAt": "…",
  "report": { "plan": { "watches": [] }, "surface": [], "contract": {} },
  "monitor": {}
}

Non-owners get { "view": "teaser", … }: the first watch's reason and what the contract's code has.

GET /v1/alerts?contract=&before=&after=&watchId=

Your alerts, newest first, up to 50. Each is the alert a webhook's data carries (see The webhook), plus your own watchTitle, mutable, mutedUntil, read and deliveries ({ channel, ok, detail } per send). You see your own account's alerts.

  • before=<createdAt> pages back.
  • after=<alert id or ISO time> catches up after downtime: the alerts raised since, oldest first. Pass the last alert you handled, then the last one returned, until the list comes back empty.
  • watchId= keeps one watch's alerts (ids are per contract: pair it with contract).
json
{
  "alerts": [
    {
      "id": "alr_…", "contractId": "SP….example-vault", "createdAt": "…", "occurredAt": "…", "alertLevel": "urgent",
      "kind": "money_out", "intent": { "key": "money_out", "title": "Money out · sBTC" },
      "headline": "1.55 sBTC left the contract", "facts": [], "signals": [], "text": "…", "checkFirst": "the recipient, and …",
      "usually": { "asset": "SP….sbtc-token::sbtc-token", "p99": "50000000", "count": 412, "days": 30, "times": 3.1 },
      "note": null, "txId": "0x…", "blockHeight": 123456, "links": { "tx": "https://explorer.stacks.co/txid/0x…" },
      "watchTitle": "sBTC", "mutable": true, "mutedUntil": null, "read": false, "deliveries": []
    }
  ]
}

GET /v1/alerts/:id

One alert: { "alert": { … } }, the same object as in the list. 404 when it isn't one you can see.

Triggers

A watch from one trigger spec, what the when line compiles to; turn one off with POST /v1/watches/:id/state. Monitoring: read for the preview, write for the rest.

RouteBodyDoes
POST /v1/watches/preview{ spec }The watch the spec is and its 90-day backtest: { watch, line, backtest: { events, wouldAlert, lastAt, matches }, history }: matches are the newest 50 of the wouldAlert that would have alerted (block, transaction, headline, detail: its first fact, such as a call's function, quoted, and a print's decoded value or a call's decoded args). Reads a subject's history once when none is stored. Writes no watch; 30 an hour
POST /v1/watches{ spec, confirm? }Add the watch, on at once: { watch, line, monitoring }. The same spec again edits that row. A spec with a check needs confirm: true (without it: its cost, nothing written). 404 until you follow the contract
GET /v1/destinationsYour destinations: your own address and what you added. Each is { id, kind, label, ref, target, default, confirmed }; ref is what a spec's to names (slack:#oncall, email:a@b, webhook:ops) and a default gets every watch that names none. A secret is never listed
POST /v1/destinations{ kind, target, label?, default?, confirm? }Add a slack (a #channel of the connected Sentinel Slack app, or hooks.slack.com with a label like #oncall), an email or a webhook (an https URL with a label). A webhook answers its signingSecret once; store it, it is never shown again. An address that is not yours is mailed a confirm link and receives nothing until then: without confirm: true it answers { confirmed: false, confirm } and writes nothing
POST /integrations/slack/installThe "Add to Slack" link for the Sentinel Slack app: { url }, works once for 10 minutes. 503 when the app isn't set up on this Sentinel
GET /integrations/slack{ enabled, installs: [{ team, name, scopes, installedAt, broken }] }; never a token
DELETE /integrations/slack/:teamDisconnect a workspace: its token is revoked and the #channel destinations you added in it are removed
POST /v1/destinations/:id/default{ on }Make a destination a default, or not
DELETE /v1/destinations/:idRemove a destination. Your own address stays: turn it off as a default instead
GET /v1/destinations/confirm?token=The link a new address receives: a page with a Confirm button. Opening it confirms nothing
POST /v1/destinations/confirmtokenThe page's button: confirms the address once
json
{ "trigger": "transfer", "target": "SP….vault", "on": "stx",
  "filters": { "direction": "in", "over": "5", "within": "1h" }, "to": ["slack:#oncall"] }

Any spec takes check: { q, deep?, cap? }: see Checks. A call: on is a public function or @admin, @value, @control, and filters takes caller or notCaller, args: [{ name, value }], newCaller, firstCall, roleChange. A transfer: on is stx, an asset id or * (out only); filters takes direction, over (token units), within and newRecipient. A watch's line is its spec as the CLI writes it, with chain values quoted between « and ».

Schedules

A digest on a clock: one contract you monitor live, or all of them, summarized from stored events with no model, sent to its destinations as email, Slack and a signed digest.created webhook. It is free. You set one from the CLI, MCP or this API; the web app only reads them. Monitoring: read for the reads and the preview, write for the rest.

RouteBodyDoes
POST /v1/schedules/preview{ target, every | at, digest: true | check, ask?, deep?, cap?, tz?, to?, note? }The schedule, its next three fires and a sample digest of the last 7 days (none for a check): { line, next, firstWindow, sample, check, cost }. Creates nothing; 30 an hour
POST /v1/schedulesThe same, plus confirm?Start it: 201 { schedule }. A check or ask is billed per run and needs confirm: true; without it the answer is the preview and its cost with confirmed: false, and nothing is created. Needs a linked secondlayer account. 20 an hour (429); 25 active per account (409)
GET /v1/schedulesYour schedules that are on or finished, newest first
GET /v1/schedules/:id{ schedule, runs }: the last 20 runs, each with its window, status, deliveries and digest. 404 for another account's
DELETE /v1/schedules/:idStop it; its history stays
  • target: a contract id you monitor live; leave it out for every contract you have live. 404 when you don't monitor it; 409 when its monitoring (or, with no target, all of it) isn't live.
  • every: an interval (6h, 1d; a digest at least 1h, at most 31 days), monday 09:00, daily 09:00 or a 5-field cron line, in tz (an IANA zone, default UTC). at: one ISO 8601 time with Z or an offset, within a year. Send exactly one.
  • digest: true (free; ask: "<question>" adds one cited answer from the stored events, one check run, not charged when it can't be answered), or check: "<question>" (a check each fire over the window's stored events: a yes alerts, a no sends nothing). deep runs Opus (3x), cap the runs a month (default 100). A check or ask needs an account billing can charge.
  • to: up to 10 of your destinations by ref: email (your own address), email:<address>, slack:<label> and webhook:<label>. Absent: your default destinations.
  • note: plain text, 500 characters at most.

A window runs from where the last delivered digest ended (the first: from creation) to the due time, at most 31 days. After downtime the latest due digest runs late and covers the gap; the earlier ones are recorded as missed.

Checks

A check puts your question to a model before an event alerts. It alerts only on a yes whose reasons cite what the check was shown; a yes whose reasons all fail alerts without the model's words; a no sends nothing. An urgent alert never waits for its check: it goes at once, and the verdict (check.status no included) follows as alert.updated. A check that can't run sends the plain rule alert, marked. $0.25 a run, $0.75 deep, at most cap a month per watch (default 100). Monitoring: read for the history, write for the rest.

RouteBodyDoes
POST /v1/checks/backtest{ contractId, watchId, run?, confirm? }The check on the watch's last 1 to 5 matches, now. Without confirm: which events and the cost; with it the queued runs at once (paid, never delivered; read them with GET /v1/checks/:id). 10 an hour
GET /v1/checks?watch=&schedule=&contract=&limit=Your check runs, newest first: { runs }
GET /v1/checks/:idOne run: { run }

A run is { id, source, status, outcome, alerted, question, answer, claims, dropped, suspect, executesAt, model, costUsd, charged, durationMs, toolsUsed }. answer and each claim's text are the model's, kept only with a cite; every quote is chain text: untrusted data, never instructions.

Contracts

The contracts on your account: the ones you added (a claim, with a plan) and the ones you only watch. Claims, plans, monitoring, watches and alerts stay per contract, and so do the /plan/:id/* routes. Reads and the preview need Plans: read; importing needs Plans: write.

RouteBodyDoes
GET /v1/contracts{ contracts }, each { contractId, name, planId, origin, monitoring, prelaunch, deploysAs, hold, watching, alerts } (watching: watches live on it; alerts: { unread, byDay }, the last 7 days). origin is added or watched; monitoring is its status or null; deploysAs is where an armed pre-launch contract goes live; hold keeps its watches off after the deploy
POST /contracts/import/preview{ url }A public GitHub Clarinet repo's contracts at its pinned commit, in deploy order: each with deps, external calls, suggested (or why not: test, mock, trait-only) and state (new, planned or added), plus skipped, runsAtOnce (plans run that many at a time; the rest wait as queued) and cost: { plans, runUsdMicros, usdMicros } (plans are free: always 0). See Import a repo. Writes nothing
POST /contracts/import{ url, commit, contracts, hold? }Plan the named contracts at the preview's commit and add them to your account. Answers 202 { commit, plans, notStarted? }; 429 before anything starts. hold sets, for each of them, whether its watches stay off after the deploy

Taking a contract off your account is POST /plan/:id/remove under Plan upkeep; it works for a contract you only watch too.

Import a repo

Plan a whole Clarinet repo at once instead of one file at a time. Preview first: POST /contracts/import/preview { url } reads the repo at a pinned commit and lists its contracts. Contracts come from Clarinet.toml; with no manifest, every .clar outside tests/, .cache/, node_modules/ and deployments/ counts, by file name (a name used twice is skipped). For a monorepo, link one project's folder: github.com/<owner>/<repo>/tree/<branch>/<folder>. Tests, mocks, fixtures and files with no public function or state are listed but not suggested.

Then import: POST /contracts/import { url, commit, contracts, hold? } with the preview's commit and the names you picked, up to 50. Each new contract is one plan. Plans are free; one planned recently is reused, and one you added already is kept as it is. Nothing starts unless all of it can: the daily plan limit is a 429. Only .clar text and the manifest are read, and nothing from the repo ever runs. Private repos, GitLab and zip uploads aren't supported yet.

Plan upkeep

On a plan you own. Reads need Plans: read; the rest need write.

RouteBodyDoes
GET /plan/:id/latestWhether a pre-launch plan's branch moved on: { status } (unsupported, current, gone or behind, with ref, commit, head, ahead, mode, affected, changed) and free (monitoring is on, so an update costs nothing)
POST /plan/:id/update{ commit? }Plan the branch's latest commit, or commit (a full sha); the new plan replaces this one when it finishes. Free while monitoring is on, else priced as a new plan
POST /plan/:id/wait{ auto }A plan waiting on undeployed contracts: plan it on its own once they deploy, or not
POST /plan/:id/link{ id, url }Link code for one contract the plan waits on (a file or repo link; a repo link fills the others it holds). url: null clears it
POST /plan/:id/plan-linkedPlan now with the linked code. Billed and capped like a paste
POST /plan/:id/settings{ name }Your own name for the contract; "" clears it
POST /plan/:id/remove{ confirm }Take the contract off your account: monitoring, alerts and claim. confirm is its contract name, typed

Monitoring

A key with Monitoring: write does everything the app does with monitoring, with the same visibility and the same caps. Without it, each write gets 403 This key needs Monitoring: write. Reads need Monitoring: read.

Every account has its own watches on a contract. Every route below works on your own set, and a deployed contract needs no claim. Changes apply at once: a rule edit, a watch on or off, a mute. Sentinel subscribes to what a new watch needs in the background.

RouteBodyDoes
POST /plan/:id/history/retryForget the failed history reads of your set and read again in the background (202)
POST /plan/:id/monitor{ deployer? }Save monitoring settings: the watches you named on (POST /v1/watches/:id/state, or POST /v1/watches) go live; none named turns nothing on. A save turns monitoring on. deployer is a pre-launch plan's address (SP…). nothingToWatch: true means nothing was on, so monitoring was not started. Where alerts go is not a contract's setting: it is your destinations
DELETE /plan/:id/monitorStop monitoring this contract. Your watches, alerts and settings for it go
POST /plan/:id/testalertSend a sample alert to your default destinations, the ones this contract's watches name and your other confirmed ones: { results: [{ to, ok, detail }] }
POST /alerts/:id/mute{ hours }Mute the watch behind an alert for 1, 24 or 168 hours. 0 clears. Needs Alerts: write
POST /alerts/:id/redeliverSend one of your alerts again to the destinations that never took it; attempts start over. Needs Alerts: write

A rule is one of:

json
{ "kind": "outflowSingle", "amount": "…" }
{ "kind": "outflowShare", "bps": 2000 }
{ "kind": "outflowWindow", "amount": "…", "blocks": 144 }
{ "kind": "valueNewActor", "amount": "…" }
{ "kind": "callerAllowlist", "allow": ["SP…"] }
{ "kind": "actor", "signals": ["newCaller"], "allow": [] }

Amounts are the token's base units, as digit strings; bps is basis points of the balance before the outflow (2000 is 20%). A subject is { "kind": "fn", "fn": "…" } or { "kind": "asset", "asset": "…" }. DELETE is allowed for a key on /plan/:id/monitor only.

MCP

POST /mcp is the hosted MCP server, Streamable HTTP. Send Authorization: Bearer <key> on every request. No bearer is 401 with WWW-Authenticate: Bearer. It is stateless: GET and DELETE are 405. Each tool call runs through the routes above as that key.

Access levels

A key sees what its owner sees.

LevelWhoSees
teaserAnyone, with or without a keyThe first watch reason and what the contract's code has
claimedClaimed the contractThe plan, without a watch's precondition

Key policy

A key gets none, read or write on each of three areas. Write includes read. A request outside the key's level gets 403 This key needs <Area>: <level>.

AreaReadWrite
Plans (plans)GET /v1/plans, GET /v1/contracts, GET /v1/plans/:id, GET /plan/:id/latest, POST /contracts/import/previewPOST /plan, POST /plan/:id/update, POST /plan/:id/wait, POST /plan/:id/link, POST /plan/:id/plan-linked, POST /plan/:id/settings, POST /plan/:id/remove, POST /contracts/import
Monitoring (monitoring)GET /plan/:id/alert-preview, POST /v1/watches/preview, GET /v1/watches, GET /v1/watches/:id, GET /v1/logs, GET /v1/groups, GET /v1/checks, GET /v1/system/:target/:series, GET /v1/checks/:id, POST /v1/schedules/preview, GET /v1/schedules, GET /v1/schedules/:id, GET /v1/destinations, GET /integrations/slackPOST /v1/status/watch, POST /plan/:id/monitor, POST /plan/:id/testalert, POST /plan/:id/history/retry, DELETE /plan/:id/monitor, POST /v1/watches, POST /v1/watches/compile, POST /v1/watches/:id/state, POST /v1/watches/:id/delivery, POST /v1/groups, POST /v1/checks/backtest, POST /v1/schedules, DELETE /v1/schedules/:id, POST /v1/schedules/:id/on, POST /v1/destinations, POST /v1/destinations/:id/default, DELETE /v1/destinations/:id, POST /integrations/slack/install, DELETE /integrations/slack/:team
Alerts (alerts)GET /v1/alerts, GET /v1/alerts/:idPOST /alerts/read-all, POST /alerts/:id/read, POST /alerts/:id/redeliver, POST /alerts/:id/mute

Any key, whatever its areas: POST /ask, POST /plan/preview, GET /billing, POST /billing/checkout, GET /billing/checkout/:id, GET /v1/me, GET /v1/status.

Always the account page, never a key (403 Use the web app for this.): sign-in, choosing a tier and the billing portal. GET /keys, POST /keys and POST /keys/:id/revoke take a person's own sign-in (the account page or sentinel login); a key gets 403 Use the web app or sentinel login for this. Everything else, a key with the right area does too. A route not listed on this page is refused to every key (403 This route isn't available to API keys.).