Four calls, one guarantee
Quote a change to learn what it costs and how risky it is. Execute to authorize payment and run it. Poll for status. Roll back on demand. Every response is JSON; every failure carries a stable error.code.
- POST
/api/quotePrice a change, get its risk - POST
/api/executeAuthorize payment and run it - GET
/api/runs/:idPoll status and artifacts - POST
/api/runs/:id/rollbackUndo on demand - GET
/api/runsList the account's runs - GET
/api/metaEnums and deployment info
Authentication
Agents authenticate with a token created on the tokens page. Send it as a bearer token. Each token carries its own spending policy: a per-run ceiling, a daily ceiling, a list of approved repositories, and whether production is permitted at all. Those limits are checked when you request a quote and again when you execute.
Authorization: Bearer sc_live_8f21_a1b2c3d4e5f6…POST /api/quote
Describe the change. You get a price, a risk score, and a run_id held in quoted state. Nothing is applied and nothing is charged. Policy violations surface here rather than at execute time, so an agent learns it is out of bounds before doing any work.
POST /api/quote
Authorization: Bearer sc_live_8f21_…
Content-Type: application/json
{
"kind": "supabase_migration",
"repository": "northwind/checkout-api",
"target": "production",
"summary": "Retire the legacy shipping address constraint",
"payload": "BEGIN;\n\nALTER TABLE public.orders\n DROP CONSTRAINT orders_shipping_address_text_check;\n\nCOMMIT;",
"files_changed": 4
}201 Created
{
"run_id": "run_9c2f5e10",
"status": "quoted",
"risk": {
"level": "high",
"score": 64,
"factors": [
{ "code": "target_env", "label": "Targets production", "weight": 30, "detail": "…" },
{ "code": "sql_drop_constraint","label": "Drops a constraint", "weight": 14, "detail": "…" },
{ "code": "no_down_migration", "label": "No down migration supplied", "weight": 12, "detail": "…" },
{ "code": "kind_migration", "label": "Schema migration", "weight": 8, "detail": "…" }
]
},
"quote": {
"total_cents": 850,
"lines": [
{ "code": "protection_base", "label": "Protected run", "amount_cents": 200 },
{ "code": "checkpoint", "label": "Database checkpoint", "amount_cents": 180 },
{ "code": "monitoring", "label": "Post-apply monitoring (production)","amount_cents": 150 },
{ "code": "risk_surcharge", "label": "Risk surcharge (high, 64/100)", "amount_cents": 320 }
]
},
"expires_at": "2026-07-27T14:22:11.000Z"
}Risk scoring is a rule table, not a model. Each factor carries a fixed weight and an explanation, the weights sum to a 0–100 score, and the same input always produces the same result. Price is a pure function of that score, so an agent can predict its own spend.
POST /api/execute
Authorize payment from the credit balance and run the protected pipeline: checkpoint, gates, apply, monitor. The call returns once the run has reached a terminal state: committed, rolled_back, or failed. Send max_price_cents to refuse execution if the quote no longer matches what you agreed to.
POST /api/execute
Authorization: Bearer sc_live_8f21_…
Content-Type: application/json
{
"run_id": "run_9c2f5e10",
"max_price_cents": 1000
}200 OK
{
"run_id": "run_9c2f5e10",
"status": "rolled_back",
"outcome": { "committed": false, "rolled_back": true, "failed": false },
"charged_cents": 850,
"refunded_cents": 0,
"credit_balance_cents": 5570,
"checkpoint": {
"id": "ckpt_k3f9a21b7c",
"kind": "postgres_snapshot",
"restore_command": "npx supabase db restore --project-ref … --snapshot ckpt_k3f9a21b7c",
"retained_until": "2026-08-03T09:14:02.000Z"
},
"rollback": {
"trigger": "monitor_breach",
"reason": "p95 query latency reached 940ms against a 120ms threshold (baseline 31ms).",
"restored_checkpoint_id": "ckpt_k3f9a21b7c",
"duration_ms": 31420,
"evidence": [ "…" ]
}
}committedGates green, monitors inside baseline. Charged in full.
rolled_backApplied, a monitor breached, checkpoint restored. Charged in full; the rollback is the product working.
failedA gate blocked it pre-apply. Nothing shipped. Charged in full: catching it is the outcome you were paying for.
GET /api/runs/:id
Full status with every artifact: test gates and their console output, the checkpoint, monitor readings against baseline, rollback evidence, and the timeline. Readable by any caller holding the run id, so a polling loop stays a single unauthenticated GET.
GET /api/runs/run_9c2f5e10
200 OK
{
"run_id": "run_9c2f5e10",
"status": "rolled_back",
"terminal": true,
"risk": { "level": "high", "score": 64 },
"tests": [ { "name": "shadow-apply", "status": "passed", "duration_ms": 8412, "output": [ "…" ] } ],
"monitors": [ { "metric": "pg_p95_ms", "baseline": "31ms", "observed": "940ms", "status": "breached" } ],
"rollback": { "trigger": "monitor_breach", "…": "…" },
"timeline": [ { "at": "…", "phase": "checkpoint", "detail": "…", "level": "success" } ]
}POST /api/runs/:id/rollback
Automatic rollback already covers monitor breaches. This is the escape hatch for everything the monitors cannot see: a product decision, a customer report, a downstream system that only noticed an hour later. Restores the same checkpoint and records the same evidence trail. Only valid on a run in committed state; reason is required.
POST /api/runs/run_4b81c7a2/rollback
Authorization: Bearer sc_live_8f21_…
Content-Type: application/json
{
"reason": "Support reports checkout failing for EU customers since this shipped."
}A complete agent loop
Everything an agent needs, end to end: price it, decide, authorize, and read back what happened.
import { setTimeout as sleep } from 'node:timers/promises'
const API = 'https://safecommit.dev/api'
const headers = {
Authorization: `Bearer ${process.env.SAFECOMMIT_TOKEN}`,
'Content-Type': 'application/json',
}
export async function protectedChange(change) {
// 1. Price it. Nothing is applied and nothing is charged.
const quote = await fetch(`${API}/quote`, {
method: 'POST', headers, body: JSON.stringify(change),
}).then((r) => r.json())
if (quote.error) throw new Error(quote.error.message)
// 2. Decide. The risk assessment is a rule table, not a model;
// the same input always produces the same score.
if (quote.risk.level === 'critical') {
return { skipped: true, reason: 'critical risk', risk: quote.risk }
}
// 3. Authorize. Returns once the run has reached a terminal state.
const run = await fetch(`${API}/execute`, {
method: 'POST',
headers,
body: JSON.stringify({
run_id: quote.run_id,
max_price_cents: quote.quote.total_cents,
}),
}).then((r) => r.json())
if (run.status === 'rolled_back') {
// The change went out and came back. Read why, then try something else.
return { reverted: true, reason: run.rollback.reason }
}
if (run.status === 'failed') {
// A gate caught it before apply. The logs say which one.
const detail = await fetch(`${API}/runs/${run.run_id}`).then((r) => r.json())
return { blocked: true, tests: detail.tests.filter((t) => t.status === 'failed') }
}
return { committed: true, checkpoint: run.checkpoint.id }
}Vocabulary
quotedauthorizedrunningcommittedrolled_backfailedlowelevatedhighcriticalvercel_deploymentsupabase_migrationproductionstagingpreviewMachine-readable at GET /api/meta.
Errors
Every failure returns { "error": { "code", "message" } }. Validation failures add error.fields keyed by field name.
| 401 | unauthorized | No bearer token and no session cookie. |
| 403 | repository_not_allowed | The token is not approved for that repository. |
| 403 | production_not_allowed | The token may not target production. |
| 403 | per_run_limit_exceeded | The quote is above the token's per-run ceiling. |
| 403 | daily_limit_exceeded | The token has spent its daily allowance. |
| 402 | insufficient_credit | The account balance will not cover the run. |
| 404 | run_not_found | No run exists with that id. |
| 409 | invalid_transition | The run is not in a state that allows this call. |
| 409 | price_above_limit | The quote exceeds the `max_price_cents` you sent. |
| 422 | validation_failed | A field is missing or malformed; see `error.fields`. |
What this build actually does
This is an MVP. No production infrastructure is modified. Vercel deployments and Supabase checkpoints are simulated: the test logs, checkpoint ids, monitor readings and rollback evidence are generated by a deterministic simulator (src/lib/simulate.ts) as a pure function of the run, so the same run always renders identical artifacts.
Everything around it is real and unchanged by that substitution: the risk rules, the pricing, the spending policy, the credit ledger, the state machine, and the API surface. Swapping the simulator for adapters that call the Vercel and Supabase Management APIs leaves every consumer working against the same RunArtifacts shape.
persistence: supabase · billing: placeholder · execution: simulated