A deterministic Datalog policy gate for agent payments.
When an AI agent tries to spend money, you should not have to trust its explanation. This
project puts a small, mechanical policy kernel between the agent and the payment: every
spend request is reduced to a closed set of facts pulled from authoritative sources
(never the model's narration), and those facts are ground through a Datalog program that
returns allow / deny the same way every time. Same facts → same answer, and the facts
are true.
The canonical pitch is
PITCH.md. (hackathon-plan.htmlandpitch-deck.htmlderive from it — seeCLAUDE.md→ "Keeping the pitch in sync".) To run the live demo end to end, followLAUNCH.md.
All four pillars are built and enforced. One-liner: everyone else scopes the card; we prove the decision.
pnpm install && pnpm db:reset && pnpm build && pnpm test # 575 tests
pnpm demo # drive every pitch beat through the REAL hook; assert exit codes
pnpm proof # independently re-verify the bundles the gate sealed- The model never decides. The gate reads the amount, vendor, category, spend-so-far, caps, and clearances from the ledger DB + vendor registry + structured tool args — the agent's free-text reasoning is used only as keys to look things up, never as facts.
- The kernel is deterministic and pure.
evaluate(facts) → decisionhas no clock, no I/O, no randomness. Identical facts always yield an identical decision.denydominates: any single deny rule denies the spend. - Enforcement is fail-closed. A
PreToolUsecommand hook intercepts the payment tool before it runs; any bad input, unreachable DB, or kernel error emits a deny and exits non-zero. There is no "fail open" path. - It's auditable — and the audit is re-derivable. Every decision is sealed into a
content-addressed provenance bundle: the decision, the exact facts, and for each fact the
specific query / notary / declassifier it came from.
pnpm proofre-runs the kernel on the recorded facts and checks the verdict falls out. An audit log is a claim a system writes about itself; a bundle can be checked by someone who trusts nothing. You cannot reseal your way out of arithmetic. - Untrusted content can't act. Invoices and emails are wrapped at the boundary in a value that
refuses to become a string (
${q},String(q),JSON.stringify(q)all throw). It escapes only through a total declassifier into a bounded codomain — so an attacker's reachable set is a number we chose in advance, not "strings we failed to imagine." - Invoices are authenticated, not just matched. A notary-signed statement binds the invoice
bytes, the amount, and the vendor's registered domain, verified before money moves. A 3-way
match compares documents to each other; three consistent forgeries pass it. (Scope stated plainly
in
packages/attestation/README.md: real Ed25519 and real binding checks, not the TLSNotary MPC protocol.)
A request flows down through every pillar before a dollar moves. Enforcement comes from the topology, not from the agent cooperating.
agent calls mcp__payments__pay_vendor
│
▼
┌──────────────────────────────┐ raw SpendRequest — ALL of it untrusted transport,
│ hook/evaluate.mjs │ including the attestation blob. Used only as keys.
│ (fail-closed hook) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐ PILLAR 3 — invoice + invoiceRef wrapped at the
│ @ramp/quarantine │ boundary. Cannot become a string. Escapes only
│ (CaMeL: data cannot act) │ via a total declassifier into a bounded codomain.
└──────────────┬───────────────┘
│ a digest — never the bytes
▼
┌──────────────────────────────┐ PILLAR 4 — Ed25519 signature vs a trusted notary
│ @ramp/attestation │ keyring, AND binding: invoice digest, the vendor's
│ (authenticate, don't match) │ REGISTERED domain, amount, currency, freshness.
└──────────────┬───────────────┘
│ attestation_present: a verified BOOLEAN (never a claim)
▼
┌──────────────────────────────┐ AUTHORITATIVE facts:
│ @ramp/ledger (SQLite) │ vendor_verified, daily_total_so_far, caps,
│ + vendor registry │ approved + cleared categories — and the exact
└──────────────┬───────────────┘ SQL it ran, recorded as provenance.
│ Facts (the frozen contract, @ramp/shared)
▼
┌──────────────────────────────┐ PILLAR 1 — policy.dl (Souffle) ⇄ TS reference kernel
│ @ramp/gate PolicyKernel │ deny dominates; deterministic, pure, no clock.
└──────────────┬───────────────┘
│ Decision { decision, reasons, firedRules }
▼
┌──────────────────────────────┐ PILLAR 2 — seal a content-addressed bundle:
│ @ramp/provenance │ decision + facts + where every fact came from.
│ (re-derivable, not logged) │ `pnpm proof` re-runs the kernel and checks.
└──────────────┬───────────────┘
│
exit 0 = allow · exit 2 = deny ──► @ramp/dashboard re-verifies in your BROWSER
Where the clock lives. Freshness needs wall time, so exactly one place reads it: the hook, in the
fact-gathering layer, alongside the DB reads. It passes now into the attestation verifier (which
stays pure), and only the resulting boolean crosses into the kernel. Gathering facts may read the
world; deciding may not. That split is what keeps "same Facts → same Decision" true — and it's
what makes pillar 2's re-derivation possible at all.
The seam between "facts" and "allow/deny" is a single interface, PolicyKernel, with two
implementations behind it: a TypeScript reference kernel (the golden oracle, always
available, zero deps) and an optional WASM kernel — a hand-written Rust mirror of policy.dl
compiled to WebAssembly (not Soufflé-generated; policy.dl is the spec every mirror is checked
against). Callers are implementation-agnostic; a parity test cross-checks the two on the golden
cases and 4000 randomized fact sets, and it runs in CI.
| Workspace | Path | Depends on | What it is |
|---|---|---|---|
@ramp/shared |
packages/shared/ |
— | The frozen contract: Facts, Decision, RuleId, PolicyKernel, SpendRequest, fact translation, canonicalJson. Zero runtime deps; browser-safe; imported by everyone. |
@ramp/gate |
packages/gate/ |
@ramp/shared |
Pillar 1 — the policy kernel (hero). policy.dl (Datalog) is the spec; the TS reference kernel mirrors it line-for-line; an optional hand-written Rust→WASM kernel is the parity-checked 4th expression. |
@ramp/provenance |
packages/provenance/ |
@ramp/shared |
Pillar 2 — decision bundles + verifyBundle, the auditor's function. Does not depend on @ramp/gate: an auditor brings their own kernel. |
@ramp/quarantine |
packages/quarantine/ |
— (node:crypto) |
Pillar 3 — the CaMeL wrapper + total declassifiers into bounded codomains. |
@ramp/attestation |
packages/attestation/ |
@ramp/shared |
Pillar 4 — Ed25519 notary attestation, canonical domain-separated signing, binding checks. |
@ramp/ledger |
packages/ledger/ |
shared + provenance + gate | The authoritative fact source (SQLite) + vendor registry, the append-only decision log, proofs, the read-only bridge, and the policy simulator. Pure DB reads — never model narration. |
@ramp/client |
packages/client/ |
shared + gate + ledger + attestation | The typed agent SDK — build a provable spending agent in a few lines. A convenience over the real lifecycle, not a bypass. |
@ramp/payments-mcp |
apps/payments-mcp/ |
shared + ledger + attestation | Self-enforcing MCP tool (mcp__payments__pay_vendor) + 4 read-only agent tools — drives the purchase lifecycle itself, so it is safe even with no hook present. |
@ramp/dashboard |
apps/dashboard/ |
shared + provenance + gate | Vite + React. The Audit page re-verifies bundles in your browser with WebCrypto and the real kernel; plus a read-only Pricing reference tab. |
@ramp/control-plane |
apps/control-plane/ |
ledger | Demo-only process (pnpm control-plane) — live model pricing, UI-triggered real gated transactions, and typed INPUT-table admin (create agent, retune policy dials). Not the audit bridge, not the gate. |
| The gate | hook/ (+ .claude/ shim) |
all of the above | The fail-closed PreToolUse enforcement point — the ONLY place policy is enforced. |
Ownership: @Srujyama owns the gate + the three pillars + shared contract + repo wiring;
@neilporw owns the ledger fact source + payments MCP stub; @JonKach owns the dashboard shell. See
.github/CODEOWNERS.
# Prereqs: Node >= 24 (see .nvmrc) and pnpm 11.13.0 (corepack enable).
nvm use
corepack enable
pnpm install # install the whole workspace
pnpm db:reset # build the demo ledger from schema.sql + seed.sql
pnpm build # tsc build every package
pnpm test # run every workspace's node:test suite (544 tests)
pnpm demo # drive EVERY pitch beat through the real hook, assert exit codes
pnpm proof # independently re-verify the bundles the gate just sealed
pnpm stats # operator view: allow/hold/deny rates, money stopped, what's catching
pnpm explain [<id>] # why a payment was stopped + the KERNEL-CONFIRMED counterfactual
pnpm simulate [<f>] # pre-flight a batch through the real kernel (zero side effects)
pnpm policy-diff # replay the log under a hypothetical policy dial (-- --cap 300)
pnpm receipt # emit a self-contained .mjs proof anyone can run with just node
pnpm redteam # fire the attacker's playbook at the real gate; every attack must be blocked
pnpm dev # dashboard (Vite) — the Audit page re-verifies in your browser
pnpm mcp # (separately) run the stub payments MCP serverpnpm proof verifies bundles — by importing our code, from our repo. If you're auditing us that's
worth nothing: you'd be asking the thing under audit whether it's honest. So there's one file with
zero dependencies you can copy anywhere:
cp verify-ramp-proof.mjs /somewhere/empty/
cd /somewhere/empty && node verify-ramp-proof.mjs bundles/ # no install, no network
node verify-ramp-proof.mjs bundles/ --gate-key gate-public.pem # also check authenticityIt re-derives every decision from its own recorded facts and checks the recorded verdict falls out.
You don't have to trust our monorepo — only ~300 lines you can read in ten minutes, and node. CI
runs it from an empty directory, so the "zero deps" claim can't quietly rot.
It's a second kernel, which is a real risk: two implementations can disagree, and a verifier that disagrees with the gate is worse than none. That's handled the same way the repo handles its WASM kernel — a parity test cross-checks it against the reference oracle on the golden cases and 5000 randomized fact sets, and CI fails on a single character of drift.
pnpm test is not the bar; pnpm demo is. The tests prove the kernel works. pnpm demo
spawns hook/evaluate.mjs as a real subprocess — exactly how Claude Code invokes it — and asserts
the exit code, which is the actual contract with Claude Code. A green kernel behind a broken
hook is a broken product: that is exactly how a fail-open allowing a $400 over-limit payment once
survived a fully green suite. Both pnpm demo and pnpm proof run in CI, so the pitch is
executable and cannot quietly drift into fiction.
The demo scenario is seeded: agent agent_47, verified vendor acme_corp, approved
category office_supplies, per-transaction cap 500, daily limit 1500, prior spend today
1140. The hero happy path — a 340 request — allows (1140 + 340 = 1480 <= 1500).
Deny beats are seeded off the same DB: unverified vendor (sketchy_llc), unapproved category
(crypto), and approved-but-uncleared (travel, which agent_47 isn't cleared for).
Seed note: the prior total is
1140, not1200— deliberately, so the happy path allows. Don't "fix" it. Details inpackages/ledger/sql/seed.sqlandCONTRIBUTING.md.
The TS reference kernel is always the default, so nothing below is required to run the gate. But
building the WASM kernel and cross-checking it is a real CI job (wasm kernel — 4-way parity):
rustup target add wasm32-unknown-unknown # one-time
cargo install wasm-pack # one-time (or the official installer)
pnpm --filter @ramp/gate build:wasm # hand-written Rust kernel → WebAssembly
pnpm --filter @ramp/gate test:parity # reference ⇄ WASM on golden + 4000 random casesThe WASM kernel is a hand-written Rust mirror of policy.dl (not Soufflé-compiled). It needs only
cargo + wasm-pack + the wasm32 target — no Soufflé, no C++ toolchain.
- @Srujyama
- @neilporw
- @JonKach
Read CONTRIBUTING.md — it covers the toolchain, the PR flow,
workspace ownership, and (importantly) the parts of the contract that are frozen and must
not drift.