{
  "markdown": "<h1 align=\"center\">\n  <img src=\"docs/assets/banner.svg\" alt=\"Ratchet — an effect gate for AI agents\" width=\"860\">\n</h1>\n\n[![CI](https://github.com/thearchitect0x-glitch/ratchet/actions/workflows/ci.yml/badge.svg)](https://github.com/thearchitect0x-glitch/ratchet/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/thearchitect0x-glitch/ratchet/actions/workflows/codeql.yml/badge.svg)](https://github.com/thearchitect0x-glitch/ratchet/actions/workflows/codeql.yml)\n[![npm](https://img.shields.io/npm/v/ratchet-mcp?label=ratchet-mcp)](https://www.npmjs.com/package/ratchet-mcp)\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/thearchitect0x-glitch/ratchet/badge)](https://scorecard.dev/viewer/?uri=github.com/thearchitect0x-glitch/ratchet)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14440/badge)](https://www.bestpractices.dev/projects/14440)\n[![NIST SSDF](https://img.shields.io/badge/NIST%20SSDF-conformance-informational)](docs/SSDF.md)\n[![REUSE](https://api.reuse.software/badge/github.com/thearchitect0x-glitch/ratchet)](https://api.reuse.software/info/github.com/thearchitect0x-glitch/ratchet)\n\n**An effect gate for AI agents.** Your agent asks before it does anything it cannot take back —\ncharge a card, ship a deploy, publish a package, send the email — and gets a durable decision, so\nthe same real-world action is attempted at most once across crashes and retries. Agents can also\nread back what a run already did, and spend against a limit they cannot raise.\n\nAgents retry. LLM control flow is non-deterministic, network calls fail ambiguously, and processes\ndie mid-action. The result is duplicate emails, double charges, and repeated writes — and nothing\nin the stack knows which. Vendor idempotency keys help for the few vendors that offer them, and\nnever across separate agent processes or model providers.\n\nRatchet does not execute your actions. It holds a durable decision record in front of them.\n\n```\nPOST /v1/effects/begin  →  decision: execute | duplicate | in_flight\n                                    | blocked | approval_required | denied\n```\n\nOnly `execute` authorises the caller to act.\n\n---\n\n\n## Project documents\n\n| | |\n|---|---|\n| [Architecture](docs/handoff/ARCHITECTURE.md) | High-level design — what the gate is and what it deliberately is not |\n| [Assurance case](ASSURANCE_CASE.md) | Threat model, trust boundaries, and the argument for each security requirement — including what is *not* defended |\n| [Roadmap](ROADMAP.md) | What the next year holds, and what will never be built |\n| [Governance](GOVERNANCE.md) | Who decides, and what happens if they stop |\n| [Contributing](CONTRIBUTING.md) | How to report a bug or propose a change |\n| [Security policy](SECURITY.md) | How to report a vulnerability, and how fast you hear back |\n| [Code of conduct](CODE_OF_CONDUCT.md) | What is expected, and who to tell |\n| [Known limitations](docs/handoff/KNOWN_LIMITATIONS.md) | Everything that is not true yet, stated plainly |\n\n## The part that matters\n\nIf your process dies between \"go\" and \"done\", most systems quietly let the next caller retry.\nRatchet won't. The lease expires and the effect becomes **`indeterminate`** — a known unknown,\nsurfaced instead of buried. What happens next is the policy you declared for that effect type:\n\n| `on_indeterminate` | Behaviour | Use for |\n|---|---|---|\n| `block` (default) | No automatic retry. A human or verifying agent resolves it. | Anything irreversible |\n| `retry` | A fresh attempt is granted, up to `max_attempts`. | Vendors that are genuinely idempotent |\n| `probe` | Caller must verify at the vendor and record evidence first. | Charges, transfers, payouts |\n\nExactly-once delivery is not achievable in a distributed system and this project does not claim it.\nWhat Ratchet guarantees is **at-most-once initiation**, a recorded outcome that later callers\nreplay, and an explicit state for the case nobody else admits exists.\n\n---\n\n## Quick start\n\nRequirements: Node 20.11+, Docker (for local Postgres).\n\n```bash\nnpm install\ncp .env.example .env          # defaults work for local development\nnpm run dev:db                # Postgres on :5433 via Docker\nnpm run migrate\nnpm run dev                   # control plane on :8787\nnpm run dev:worker            # lease reaper + webhook delivery (separate terminal)\n```\n\nThen open <http://localhost:8787>, or drive it from the shell:\n\n```bash\nbash examples/curl/walkthrough.sh\n```\n\n`npm run seed` populates a workspace with realistic state — a completed effect, a duplicate, an\nindeterminate one, and one awaiting approval — so the console has something to show.\n\n### Or with Docker Compose\n\n```bash\nAUTH_SECRET=$(openssl rand -base64 32) docker compose up --build\n```\n\n---\n\n## The core loop\n\n```bash\n# 1. Ask, before you act.\ncurl -X POST http://localhost:8787/v1/effects/begin \\\n  -H \"Authorization: Bearer $RATCHET_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"effect_type\": \"email.send\",\n    \"idempotency_key\": \"welcome:user_123\",\n    \"payload\": { \"to\": \"sam@example.com\" },\n    \"estimated_cost_micros\": 800\n  }'\n# → { \"decision\": \"execute\", \"effect_id\": \"eff_...\", \"lease_token\": \"lt_...\" }\n\n# 2. Do the real thing, yourself. Ratchet never touches it.\n\n# 3. Say what happened.\ncurl -X POST http://localhost:8787/v1/effects/eff_.../report \\\n  -H \"Authorization: Bearer $RATCHET_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"lease_token\": \"lt_...\", \"outcome\": \"succeeded\",\n        \"result\": { \"message_id\": \"msg_9f2\" } }'\n\n# Any later caller with the same key now gets:\n# → { \"decision\": \"duplicate\", \"result\": { \"message_id\": \"msg_9f2\" } }\n```\n\n**The one rule:** report `failed` only when you *know* the action did not reach the outside world.\nIf you are unsure — a timeout, a dropped connection — report nothing. The lease lapses and Ratchet\nrecords an honest `indeterminate`. A false `failed` is worse than silence, because it licenses a\nduplicate.\n\n### Idempotency keys\n\nDerive the key from the work, deterministically.\n\n| Good | Broken |\n|---|---|\n| `welcome-email:user_123` | `uuid4()` |\n| `invoice:2026-08:acct_88123` | `\"send-\" + Date.now()` |\n| `pr:acme/api:feature-auth` | `f\"job-{attempt_number}\"` |\n\nA key that changes on every attempt makes every retry look like new work.\n\n---\n\n## When Ratchet is unreachable\n\nRatchet sits in your critical path, so decide this before integrating: on an outage your agent\neither **acts without the gate** (fail-open) or **refuses to act** (fail-closed). Use fail-closed\nfor anything you would have to apologise for; fail-open where the vendor deduplicates anyway.\n\nFull contract, client patterns, and the honest availability posture:\n[`docs/FAILURE_MODES.md`](docs/FAILURE_MODES.md).\n\n## Architecture\n\n```\n                    ┌──────────────────────────────┐\n  agents ──────────▶│  control plane (stateless)   │\n  REST + MCP        │  Fastify · /v1 · /mcp · web  │\n                    └──────────────┬───────────────┘\n                                   │\n                    ┌──────────────▼───────────────┐\n                    │  Postgres                    │\n                    │  effects · policies · ledger │\n                    │  spend windows · audit       │\n                    └──────────────▲───────────────┘\n                                   │\n                    ┌──────────────┴───────────────┐\n  webhooks ◀────────│  worker (long-running)       │\n                    │  lease reaper · delivery · GC│\n                    └──────────────────────────────┘\n```\n\nThe control plane is stateless and scales horizontally — it may run on serverless infrastructure.\n**The worker may not.** It expires leases on a timer whether or not a request is in flight; a\nserverless function cannot do that. Run it as a long-running container. Multiple replicas are safe\n(every claim uses `FOR UPDATE SKIP LOCKED`).\n\nAt-most-once is enforced by a database unique constraint on\n`(workspace_id, effect_type, idempotency_key)` — not by application logic.\n\nFull detail: [`docs/handoff/ARCHITECTURE.md`](docs/handoff/ARCHITECTURE.md).\n\n---\n\n## Deploying\n\nThe control plane is stateless and can scale freely. **The worker cannot** — it expires leases on a\ntimer whether or not a request arrives, so it must be a long-running process. That single\nconstraint rules out purely serverless hosts (Vercel, Netlify functions) despite their being\neasier, and is why `fly.toml` runs both process groups from one image.\n\n```bash\nbrew install flyctl && fly auth login   # once, needs your browser\nnpm run deploy:fly\n```\n\nThe script is idempotent: it creates the app, provisions managed Postgres, generates `AUTH_SECRET`\nonce (never rotating it, since that would invalidate every API key), deploys both processes, and\nverifies readiness. It refuses to proceed unless preflight passes:\n\n```bash\nnpm run deploy:preflight\n```\n\nPreflight runs the full suite and production build, then checks that `AUTH_SECRET` is strong and\nnot the dev default, `PUBLIC_URL` is set (otherwise the manifest would advertise `localhost`),\n`RATE_LIMIT_OVERRIDE` is unset, private-network webhooks are off, CORS carries no wildcard, and —\nif Stripe is selected — that both the key and the webhook secret are present. It prints no secret\nvalues.\n\nAny container platform works; only `fly.toml` is Fly-specific. Set `DATABASE_URL`, `AUTH_SECRET`,\n`PUBLIC_URL`, `NODE_ENV=production`, then run `node dist/api/server.js` (scale freely) and\n`node dist/worker/main.js` (at least one, always on).\n\nTo rehearse the exact production containers locally:\n\n```bash\nAUTH_SECRET=$(openssl rand -base64 32) docker compose up --build\n```\n\n## Commands\n\n| Command | What it does |\n|---|---|\n| `npm run dev` | Control plane with reload |\n| `npm run dev:worker` | Worker with reload |\n| `npm run dev:db` / `dev:db:down` | Local Postgres in Docker |\n| `npm run migrate` | Apply migrations (advisory-locked; safe to run concurrently) |\n| `npm run seed` | Populate a workspace with realistic state |\n| `npm test` | Typecheck + unit + integration + e2e against a disposable database |\n| `npm run test:unit` / `test:integration` / `test:e2e` | One layer |\n| `npm run typecheck` / `lint` | TypeScript in strict mode |\n| `npm run build` | Compile to `dist/` |\n| `npm start` / `start:worker` | Run the compiled build |\n| `npm run mcp:stdio` | MCP server over stdio |\n| `npm run openapi` | Write the OpenAPI document to disk |\n| `npm run deploy:preflight` | Verify the build and configuration are safe to deploy |\n| `npm run deploy:fly` | Deploy control plane, worker, and database to Fly.io |\n| `npm run metrics` | Operating metrics against the pricing-review thresholds |\n| `npm run stripe:check` | Report payment configuration and verify it against Stripe |\n| `npm run stripe:listen` | Forward Stripe events to a local instance and print a webhook secret |\n| `npm run audit` | Production dependency audit |\n\n---\n\n## Environment\n\nEvery variable, with defaults and safety notes, is in [`.env.example`](.env.example). The two that\nare required:\n\n| Variable | Notes |\n|---|---|\n| `DATABASE_URL` | Postgres connection string |\n| `AUTH_SECRET` | 32+ random characters. Derives the API-key pepper and console session ids. **Rotating it invalidates every API key and session.** |\n\nIn `NODE_ENV=production` the process **refuses to start** if `AUTH_SECRET` is the development\ndefault or shorter than 32 characters, if `CORS_ORIGINS` contains `*`, or if\n`WEBHOOK_ALLOW_PRIVATE_NETWORK` is on.\n\n---\n\n## For agents\n\n| Surface | Path |\n|---|---|\n| OpenAPI 3.1 | `/openapi.json` — generated from the schemas the routes validate against |\n| Capability manifest | `/.well-known/agent-manifest.json` — including what Ratchet *doesn't* do |\n| Machine docs | `/llms.txt` |\n| MCP tool schemas | `/mcp/info` |\n| MCP (Streamable HTTP) | `POST /mcp` with `Authorization: Bearer <key>` |\n| MCP (stdio) | `npx -y ratchet-mcp` with `RATCHET_API_KEY` — see [`packages/ratchet-mcp`](packages/ratchet-mcp) |\n\nSeven MCP tools: `ratchet_begin_effect`, `ratchet_report_effect`, `ratchet_get_effect`,\n`ratchet_resolve_effect`, `ratchet_list_effects`, `ratchet_get_policy`, `ratchet_get_usage`.\n\nReady-to-use configs and code: [`examples/`](examples/) — Python, TypeScript, curl, Claude Desktop,\nCursor, and generic MCP over HTTP.\n\n---\n\n## Security posture\n\n- Only a **SHA-256 fingerprint** of your payload is stored — never the payload itself.\n- API keys are stored as **HMAC-SHA256 peppered with a server secret**; a database leak alone\n  yields no usable key. Comparison is constant-time and runs even for unknown prefixes.\n- Every query is **workspace-scoped**; a cross-tenant lookup returns `404`, never a hint.\n- **SSRF defence in two layers**: static URL validation, then DNS re-resolution with the socket\n  **pinned** to the checked address on every delivery attempt. Redirects are never followed.\n- Agent-supplied text is **data, never instructions**. Decisions come from stored policy and\n  database state — nothing in a payload can widen a scope, raise a budget, or change a policy.\n- Unknown request fields are **rejected**, not silently dropped.\n\nNo third-party audit has been performed. There is no SOC 2 report and no penetration test.\nDetails and threat model: [`docs/handoff/SECURITY_REVIEW.md`](docs/handoff/SECURITY_REVIEW.md).\n\n---\n\n## Pricing\n\nOne meter: a **gated effect** — the first `begin` for a given `(effect_type, idempotency_key)`.\nDuplicate suppression, in-flight checks, retries, reports, reads, policy changes, and webhooks are\nall free. You are never charged for the retry behaviour the product exists to absorb.\n\n| Plan | Price | Included effects/mo | Overage |\n|---|---|---|---|\n| Free | $0 | 1,000 | $1.50 / 1,000, prepaid credit only |\n| Pro | $29/mo | 25,000 | $1.50 / 1,000 |\n| Custom | contact | above 250,000 | negotiated |\n\nFree stops at its allowance unless you load prepaid credit; there is no automatic overage and no\ninvoice. Past roughly 19,300 effects a month, Pro is cheaper than paying credit on Free, so the\nupgrade is arithmetic rather than a wall.\n\nTwo plans, not three: three tiers assert knowledge of three customer segments, and there is no\nusage history yet to support one. See\n[`docs/handoff/PRICING_AND_DISTRIBUTION_REVIEW.md`](docs/handoff/PRICING_AND_DISTRIBUTION_REVIEW.md)\nfor the reasoning, including why the previous ladder priced a 20x usage range at one number.\n\nOverage draws from prepaid credit, so a runaway agent stops at your balance rather than generating\nan invoice. Cost model and assumptions:\n[`docs/handoff/PRICING_AND_UNIT_ECONOMICS.md`](docs/handoff/PRICING_AND_UNIT_ECONOMICS.md).\n\n### Payments\n\nStripe is fully wired: `startCheckout` creates real Checkout Sessions, and the signed\n`checkout.session.completed` webhook credits the ledger. Card details are entered on Stripe's own\npage and never reach Ratchet, and credit is applied **only** on the signed webhook — never on the\nbrowser returning to a success URL.\n\nBoth `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` are required before checkout opens. A key\nalone selects Stripe but keeps checkout closed: taking a payment that cannot be confirmed would\nleave a customer charged and uncredited. Run `npm run stripe:check` to see exactly what is\nconfigured (it prints no secret values).\n\nFor local development, get a webhook secret without a public URL:\n\n```bash\nnpm run stripe:listen     # prints whsec_… ; put it in .env and restart\n```\n\nMost of Stripe's onboarding checklist does not apply. Ratchet builds line items inline and never\nreads your Stripe catalog, so there is **no product to create**; it does not use Stripe Invoicing;\nand it creates Checkout Sessions through the API, not the no-code builder. What does matter before\nlive payments: **verify your account** (Stripe's KYC), create a **webhook endpoint** pointing at\nyour deployed `/v1/billing/webhook/stripe` and use *that* endpoint's signing secret in production,\nand decide whether you need **tax collection** (`STRIPE_AUTOMATIC_TAX`, off by default).\n\nWith no Stripe credentials at all, the built-in test adapter runs instead: no card is charged and\nno external request is made. Verified so far in **Stripe test mode only** — see\n[`docs/handoff/KNOWN_LIMITATIONS.md`](docs/handoff/KNOWN_LIMITATIONS.md) before going live,\nparticularly regarding refunds.\n\n---\n\n## Documentation\n\n| Document | Contents |\n|---|---|\n| [`PROJECT_MAP.md`](docs/handoff/PROJECT_MAP.md) | Where everything lives |\n| [`ARCHITECTURE.md`](docs/handoff/ARCHITECTURE.md) | State machine, concurrency, deployment topology |\n| [`DECISIONS.md`](docs/handoff/DECISIONS.md) | Every significant choice, with the reasoning |\n| [`API_AND_DATA_CONTRACTS.md`](docs/handoff/API_AND_DATA_CONTRACTS.md) | Endpoints, schemas, error codes |\n| [`SECURITY_REVIEW.md`](docs/handoff/SECURITY_REVIEW.md) | Threat model and implemented controls |\n| [`PRICING_AND_UNIT_ECONOMICS.md`](docs/handoff/PRICING_AND_UNIT_ECONOMICS.md) | Cost model and scenarios |\n| [`VALIDATION_REPORT.md`](docs/handoff/VALIDATION_REPORT.md) | What was tested and measured |\n| [`KNOWN_LIMITATIONS.md`](docs/handoff/KNOWN_LIMITATIONS.md) | What is not done, and what it would take |\n\n## License\n\nApache-2.0.\n",
  "bytes": 17261,
  "sha": "bac27143708672eb3c2d469b308f41f4bd38b6e255a16d7a3a9627c75f243c35",
  "repo_slug": "thearchitect0x-glitch/ratchet",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_ratchetgate_ratchet_d805145a/readme"
}