Rein
The control plane for AI agent payments. Rein sets the rules, watches every payment, and scores every counterparty — so agents can transact at machine speed without machine-speed losses.
Rein is developer tooling and middleware for the agentic payments economy (the x402 / ERC-8004 stack). It is non-custodial: Rein governs an agent's authority to spend, never the funds themselves. The whole design in one document: the whitepaper (PDF).
Status: v0.3 — all three phases have shipped their first cut, and a hosted engine runs an invited beta. Advisory SDK-mode + full observability, end to end — fully offline on mock rails, and live on real x402 rails on Base Sepolia (EIP-3009 USDC settled by the hosted x402.org facilitator — on both sides: the guarded agent and a
@reinconsole/gate-monetized vendor). The session-key signer tier — the GA enforcement architecture, where the wallet key leaves the agent entirely — ships as@reinconsole/signer. The supply side ships as@reinconsole/gate, vendor monetization middleware (Phase 2). The stack is durable:@reinconsole/storepersists the engine (agents, policies, spend, the signed decision chain), the reputation evidence, gate receipts + replay slots, and signer sessions across restarts. And@reinconsole/graph(Phase 3) turns the receipts both sides produce into explainable reputation scores that feed back into enforcement:vendorReputationLtpolicies on the agent side, payer screening at the vendor's door. Identity is on-chain:@reinconsole/erc8004keys reputation by ratified ERC-8004 registrations — verified live against the real Base Sepolia Identity Registry.
Product phases
| Phase | Name | What it does |
|---|---|---|
| 1 | Guard | Policy enforcement + observability for agent payments (demand side) |
| 2 | Gate | x402 monetization middleware for API vendors (supply side) |
| 3 | Graph | Reputation scoring over agents and vendors (the data moat) |
What's in v0.3
A complete demand-side Guard loop, runnable two ways: fully offline on mock rails (no accounts, no Docker, no chain), or live on Base Sepolia over the real x402 stack:
@reinconsole/sdk— wrap your agent's fetch once; every x402 paywall is policy-checked, receipted, and observable before a cent moves.@reinconsole/mcp— the guard as an MCP server. Point any MCP-capable harness (Claude Code, Codex-class agents) at it and its agent gets a spend-governed fetch plus read-only introspection of the rules it is under — no code changes. The authority boundary is the design: the client on the other end of the pipe is the agent, so no tool can widen the agent's own authority — no approving its own escalation, no editing a policy, no unfreezing itself, no minting a key. A test asserts the whole tool surface rather than a sample of it.@reinconsole/policy-engine— a sandboxed declarative rule engine (deny > escalate > allow > default) behind a Fastify API. Every decision is ed25519-signed and sha256 hash-chained into a tamper-evident audit log.@reinconsole/core— the canonical zod schemas: the single source of truth for DB rows, API payloads, and SDK types, with float-free decimal money math.@reinconsole/mock-rails— a simulated payment world (x402 facilitator + on-chain ledger + indexer) that reconciles spend and flags shadow spend: payments that bypassed the guard.@reinconsole/x402-rails— the real-world rails: an EIP-3009 payer (gasless for the agent — the facilitator submits the tx), a client for the hosted x402.org facilitator, a strict x402-v1 vendor, and an on-chain indexer that reconciles USDC transfers back to intents via the authorization nonce — and flags everything else as shadow spend.@reinconsole/console— a live "mission control" web UI over the whole stack: decisions, vendor-gate quotes/receipts/refusals, signer releases, settlements, and shadow spends streaming in real time; kill switch, vendor revenue panel, the live reputation scoreboard (scores, confidence, and the evidence behind them), and the tamper-evident audit chain.@reinconsole/signer— the custody tier. Wallet keys live in the signer, agents get capped, expiring session tokens, and every EIP-3009 signature is released only against an engine-signed allow voucher for the exact transfer being signed — verified offline, usable once. Where SDK mode detects bypass, this tier prevents it.- — the supply side (Phase 2). Middleware a vendor drops in front of any Node HTTP API to monetize it over x402: price routes by glob, quote strict v1 402s, cross-check + screen + replay-protect incoming payments, settle through pluggable rails (mock or the real facilitator), and keep vendor-side receipts and revenue stats. against the hosted facilitator.
1178 tests passing (plus 17 live network tests gated behind RUN_LIVE=1). The mock end-to-end demo runs 5 scenarios in under 500ms; the gate demo runs the full two-sided loop over real local HTTP; the graph demo closes the reputation loop on both sides; the Sepolia demos settle real USDC — and the identity demo registers a real agent on the Base Sepolia ERC-8004 registry.
Install
The twelve library packages are published on npm under the @reinconsole scope (MIT, Node ≥22), with provenance, by the release workflow. latest is 0.5.0 — npx @reinconsole/init (a sandbox, a payment and a refusal in one command), expiring API keys, and the engine on Postgres. No breaking changes from 0.3.0; 0.3.0 broke from 0.2.0, so read CHANGELOG.md before upgrading from that:
npx @reinconsole/init # a sandbox on the hosted engine, one paid call, one refused — no account
npx -y @reinconsole/mcp # the guard as an MCP server — a governed fetch for any MCP harness
npm install @reinconsole/sdk # agent-side guard — wrap your fetch
npm install @reinconsole/gate # vendor-side x402 monetization middleware
npm install @reinconsole/graph # explainable reputation scoring
| Package | What it's for |
|---|---|
@reinconsole/core | Canonical zod schemas — the single source of truth |
@reinconsole/sdk | Agent-side guard; wraps the x402 client |
@reinconsole/mcp | The guard as an MCP server: a spend-governed fetch for any MCP harness |
@reinconsole/init | npx @reinconsole/init: a sandbox, a paid call and a refused one, no account |
@reinconsole/policy-engine | Declarative rule engine + signed, hash-chained audit log |
@reinconsole/gate | Vendor-side x402 monetization middleware |
@reinconsole/graph | Reputation: evidence off every bus, explainable scores |
@reinconsole/x402-rails | Real rails: EIP-3009 payer + x402.org facilitator client + indexer |
@reinconsole/mock-rails | Offline x402 world: facilitator + ledger + indexer |
The custody tier (@reinconsole/signer) and persistence layer (@reinconsole/store) passed their security review and ship on npm as of 0.2.0.
Quickstart
npm install -g pnpm # if you don't have pnpm — `corepack enable` works too, but needs an admin shell on Windows
pnpm install
pnpm build # ~2 min
pnpm test # 1178 tests, fully offline, ~5 min
# Watch the whole thing work — budgets, tx caps, kill switch, shadow-spend detection:
node apps/demo/dist/index.js
# Then watch the custody tier refuse every rogue path a stolen agent could try:
node apps/demo/dist/signer.js
# Then flip to the vendor side: price routes, screen payers, count revenue:
node apps/demo/dist/gate.js
# Then close the loop: reputation scores that change what both sides enforce:
node apps/demo/dist/graph.js
Console — live mission control
A real-time web UI for the whole stack at once: the real policy engine (over HTTP), a real @reinconsole/gate fronting the world's vendor API, the session-key signer holding a custodied wallet, the reputation graph observing every bus, and the mock rails standing in for the chain. Every bus — engine, indexer, gate, signer — is merged and pushed to the browser over Server-Sent Events.
pnpm --filter @reinconsole/console dev # http://localhost:5173
What you see:
- Live activity feed — decisions (allow / deny / escalate), x402 quotes, vendor receipts, payments turned away at the gate, EIP-3009 signatures released or refused by the signer, settlements, and shadow spends — streaming in as they happen.
- Vendor gate — what the world's gated API is earning: revenue headline, per-route and per-payer breakdowns from real gate receipts.
- Reputation — the live scoreboard from
@reinconsole/graph: every vendor and payer the world has evidence on, with confidence, click-to-expand explanations (five components + the raw counts behind them), "→ engine" on vendor scores synced into policy, and "barred" on wallets the gate turns away. The graph re-syncs after every burst of evidence, so watch the world's own vendor cross the confidence floor as scenario runs accumulate. - Agents + kill switch — both custody tiers side by side (
sdkandsession-key), per-agent session spend, and a freeze/unfreeze toggle; hit Ping on a frozen agent and watch the call get denied. - Policies — the active rules in plain language.
- Audit chain — the ed25519-signed, sha256-linked decision log with a live integrity check.
- Shadow spends — unreconciled, guard-bypassing payments, flagged in red.
Click Run scenario to play the full two-sided story, paced so you can watch it unfold: a fresh SDK-tier agent makes four paid calls (quote → allow → vendor receipt → settle), trips the budget cap and the tx cap, then bypasses the guard (shadow spend); an unpaid crawler gets quoted; a replayed payment and a denylisted mule get turned away at the gate; then a session-key agent — wallet held by the signer — makes voucher-gated EIP-3009 purchases, a stolen voucher is replayed straight at the signer and refused, and the engine allows a payment the session cap still refuses: defense in depth, live. The finale closes the reputation loop on both sides: a procurement agent is denied at a sketchy vendor by the reputation-gate policy rule, served at a reputable one on the same policy, and a wallet that replayed payments at other vendors' gates two weeks ago presents a fresh, valid payment — and is turned away on reputation alone.
The session-key payments in this world are real EIP-3009 signatures, verified cryptographically (signature recovery against the quoted USDC contract domain) before the gate settles them — a forged or tampered authorization genuinely fails. For a production-style serve (built UI + API on one port): pnpm --filter @reinconsole/console build && pnpm --filter @reinconsole/console start.
Set REIN_CONSOLE_DATA_DIR to run the console world on @reinconsole/store: agents, policies, the kill switch, the decision chain, rolling budgets, and the reputation scoreboard all survive a restart (the boot seed runs once per data directory; the feed is telemetry and starts fresh). Kill the server mid-story, start it again, and run the scenario — the new agents pick up numbered names where the old ones left off, the chain extends the pre-restart hashes, and the door still turns away the offender on evidence recorded before the kill.
Run the policy engine standalone:
# PowerShell
$env:PORT="8787"; node services/policy-engine/dist/server.js
# bash
PORT=8787 node services/policy-engine/dist/server.js
With no REIN_ENGINE_API_KEY the engine binds 127.0.0.1 only, and refuses
to start on a public interface — an unauthenticated engine cannot be exposed by
accident. To expose it, set a key (REIN_ENGINE_API_KEY=rk_... plus
HOST=0.0.0.0) and pass the same secret to the SDK as apiKey;
REIN_ENGINE_AUTH=off is the deliberate override. The standalone console
follows the same rule: without REIN_CONSOLE_API_KEY it serves the dashboard
read-only on a public bind (REIN_CONSOLE_HOST=127.0.0.1 for local use with
the controls live).
Persistence: an engine that survives restarts
The in-memory engine is great for demos; @reinconsole/store makes it durable. It implements the engine's store ports on embedded Postgres (PGlite — real Postgres compiled to WASM, running in-process against a data directory; no Docker, no daemon, and the SQL carries straight over to hosted Postgres later). Writes are awaited to disk before the engine acts on them; reads stay synchronous from a hydrated working set.
# The same HTTP API as the policy engine, but durable:
# PowerShell
$env:REIN_DATA_DIR=".rein-data"; node services/store/dist/server.js
# bash
REIN_DATA_DIR=.rein-data node services/store/dist/server.js
Kill it and start it again: agents, the kill switch, policies (in evaluation order), rolling budgets ("$0.60 of the daily $1.00 already spent — before the restart"), and the decision log all come back. The ed25519 signing key is persisted too, so the hash chain continues across restarts — the first post-restart decision links to the last pre-restart hash, and verifyDecisionChain validates the whole history under one key, no seam.
The reputation graph gets the same treatment — the same store persists its evidence ledger (and the in-flight intent correlation map, so a settlement that lands after a restart is still attributed to the agent and vendor behind it). Scores are never stored: they recompute from the rehydrated evidence, byte-identical under the same clock.
# The graph HTTP API, durable (use a DIFFERENT data dir than the engine —
# two processes can't share one PGlite directory):
# PowerShell
$env:REIN_GRAPH_DATA_DIR=".rein-graph-data"; node services/store/dist/graph-server.js
# bash
REIN_GRAPH_DATA_DIR=.rein-graph-data node services/store/dist/graph-server.js
The gate and the signer ride the same store: vendor receipts, revenue stats, and burned replay slots resume (a payment settled before a kill is refused as a replay after the restart), and session grants — token hashes, per-session spend against the cap, revocations, and the burned-voucher set — survive a signer restart, so an agent holding a token keeps paying while a revoked one stays dead. Wallet private keys are deliberately never persisted (custody keys at rest belong in a KMS/HSM); deployments re-register wallets at boot.
Composing it in code is one line per side — in a single process, one store backs all four:
import { PolicyEngine } from '@reinconsole/policy-engine';
import { ReputationGraph } from '@reinconsole/graph';
import { createGate } from '@reinconsole/gate';
import { SessionSigner } from '@reinconsole/signer';
import { openReinStore } from '@reinconsole/store';
const store = await openReinStore({ dir: '.rein-data' });
const engine = new PolicyEngine(store);
const graph = new ReputationGraph({ ledger: store.ledger, intents: store.intents });
const gate = createGate({ /* routes, rails, ... */ store: store.gate });
const signer = new SessionSigner({ enginePublicKeyPem: engine.publicKeyPem, store: store.sessions });
The hosted engine (invited beta)
Rein runs a hosted policy engine at https://engine.reinconsole.com with the public console at app.reinconsole.com reading from it, and a reference vendor at vendor.reinconsole.com that sells two testnet routes through @reinconsole/gate (/testnet/v1/ping at $0.001, /testnet/v1/scores/vendor/:host at $0.005). Its Base mainnet lane — /v1/ping at $0.01, /v1/scores/vendor/:host at $0.02 — is opt-in and answers 404 until it is armed. Access is by invitation while the beta is small: an invitee gets an org, an agent, a starter policy and an org-scoped API key narrowed to that agent, which is the whole blast radius of the secret.
{
"mcpServers": {
"rein": {
"command": "npx",
"args": ["-y", "@reinconsole/mcp"],
"env": {
"REIN_ENGINE_URL": "https://engine.reinconsole.com",
"REIN_ENGINE_API_KEY": "rk_...",
"REIN_AGENT_ID": "agt_01J...",
"REIN_NETWORK_PROFILE": "testnet"
}
}
}
}
Two things to know before the first call:
- The network is a profile, not a policy. The engine maps Base and Base Sepolia onto the same chain, so policy alone cannot keep a testnet agent off mainnet.
REIN_NETWORK_PROFILE(defaulttestnet) is enforced in the guard and again in the payer: a testnet-profile install refuses a mainnet 402 before a signature exists, and a mainnet vendor is invisible to it rather than merely denied. - Advisory first, then funded. Without
REIN_PAYER_PRIVATE_KEYevery paywall answersALLOWED_BUT_UNPAID: the decision is on the chain and on the console, and nothing moved. Fund a wallet with free testnet USDC from faucet.circle.com (Base Sepolia), add the key, and the same call settles and shows up as a receipt.
Real rails: Base and Base Sepolia
Every network Rein pays on is a NetworkProfile (@reinconsole/x402-rails): chain id, USDC contract, facilitator and the EIP-712 domain, verified live against the real contract rather than asserted. Testnet settles through the hosted x402.org facilitator, which lists only base-sepolia and charges nothing. The MAINNET profile (base) defaults to Coinbase's CDP facilitator, which needs CDP API credentials (createProfileFacilitator, cdpAuthHeaders). It is not the only way to settle on mainnet: the facilitator is just a URL, and the reference vendor's mainnet lane settles keyless through PayAI when no CDP credentials are set. A keyless facilitator bills the seller its gas plus a margin, so price mainnet routes well above a settlement's cost; the reference vendor's $0.01 floor is that reasoning.
The same guard loop on a real chain — a guarded $0.01 USDC payment settled on-chain by the hosted x402.org facilitator, then a rogue payment that bypasses the guard and gets caught:
pnpm --filter @reinconsole/demo demo:sepolia
The first run generates an agent wallet into .env and prints faucet instructions — fund it with free testnet USDC at faucet.circle.com (no ETH needed; the facilitator pays gas), then run again. A full run spends $0.02 of testnet USDC and ends with two BaseScan links:
payment.settled— the guarded payment. The payer derives the EIP-3009 authorization nonce askeccak256(intent.id), USDC emits it back inAuthorizationUsedon settlement, and the on-chain indexer reconciles the transfer to the exact intent the policy engine allowed — an on-chain memo, with no fuzzy matching.shadow.spend— the rogue payment. The facilitator is not Rein-privileged, so it settles anyway — and the indexer flags the unreconciled spend.
From a real run: the settled payment · the shadow spend
And the vendor side on the same real rails — a @reinconsole/gate-priced Node API settling real USDC through the hosted facilitator while the paying agent stays under guard. One $0.01 payment, quoted, signed (EIP-3009), settled on-chain, receipted on both sides, reconciled by the on-chain indexer via the nonce memo — then the same payment replayed and burned at the door before the facilitator ever sees it:
pnpm --filter @reinconsole/demo demo:sepolia-gate # reuses the demo:sepolia wallet
From a real run: the gate-settled payment
The live test suite (RUN_LIVE=1 pnpm --filter @reinconsole/x402-rails test) exercises the same path. Behind a TLS-intercepting proxy or antivirus, point Node at your local root CA first (NODE_EXTRA_CA_CERTS) — see env.example.
The signer tier: keys the agent never holds
SDK mode is honest about its limit: an agent that holds its own key can bypass the guard, and Rein catches it (shadow spend). @reinconsole/signer removes the limit by removing the key. The agent process gets a session token — capped, expiring, revocable — and the wallet lives in the signer, which releases an EIP-3009 signature only when every gate passes:
- A valid voucher. The engine binds each decision to the exact intent it judged (
intentHashover amount, recipient, asset, chain), ed25519-signs it, and chains it into the audit log. The signer verifies the pair fully offline — a rogue agent can recompute every hash, but it cannot sign as the engine. - An exact match. The 402 requirement being signed must equal what the engine judged: recipient, amount, asset, network. A real $0.01 voucher cannot authorize a $5.00 transfer.
- Once. One decision releases one signature; replays are refused — including two concurrent requests racing the same voucher.
- Within the session. Per-payment and cumulative caps, expiry, and revocation are enforced at the signing boundary, under whatever policy says.
Session lifetime is capped, not merely configurable
A session token is delegated authority over real money, so how long one can live is a protocol invariant rather than a setting: ten days, maximum (MAX_SESSION_LIFETIME_SECONDS, tunable down via maxSessionLifetimeSeconds, never off — a non-positive value is a construction error). A stolen token therefore stops working on its own, whether or not anyone remembers to revoke it.
Asking for more is refused at creation, never silently shortened — a caller that thinks it holds a 30-day grant would schedule its rotation on the wrong clock and meet the cap mid-payment as an unexplained session_expired. And because a grant's real expiry is derived (min(expiresAt, createdAt + cap)) rather than stored, the cap also binds records a durable store hydrates from an older deployment or a looser config — no migration, nothing to keep in sync. /health advertises the cap; every session the API returns carries the effectiveExpiresAt it will actually die at.
Every release and refusal is emitted on the event bus (signature.released / signature.refused). The kill switch stops being advisory: freeze the agent and there is no allow, no signature, no payment.
pnpm --filter @reinconsole/demo demo:signer # seven scenarios, fully offline, every signature verified
Run it as a service (buildSignerServer) with the SDK's createRemoteSessionPayer, or in-process with sessionPayerFor. There is deliberately no HTTP endpoint that accepts a private key.
The service's session-admin routes require a bearer secret, and the default is no server at all:
buildSignerServer(signer, { adminToken: process.env.REIN_SIGNER_ADMIN_TOKEN });
POST /v1/sessions mints a grant with whatever cap it is asked for, against a wallet this process holds the key to — so an open admin surface is a wallet drain for anyone who can reach the port. Omitting both adminToken and the explicit adminAuth: 'off' opt-out is a construction error, thrown at build time rather than discovered in a log. POST /v1/sign stays open by design: the session token in the body is that route's credential, scoped and capped and revocable, which is the whole point of the tier.
That credential can also be a scoped API key instead of one static secret — read to list grants, admin to mint, revoke, or delete one, so a dashboard key can never create a session:
buildSignerServer(signer, { auth: new ApiKeyAuth({ store: reinStore.apiKeys }) });
Both forms may be passed together while a deployment rolls over. Back the key store with PgApiKeyStore (it is reinStore.apiKeys) rather than the in-memory default: keys are authority, and an in-memory store means a key you issued stops working at the next restart and a key you revoked comes back alive.
Gate: the vendor side of the wire
Everything above governs the agent spending. @reinconsole/gate is Phase 2 — the same loop from the vendor's seat. Price your routes once, and every x402 payment into your API is quoted, cross-checked, screened, settled, and receipted before your handler runs:
import { createGate, gateMiddleware, facilitatorClientRails } from '@reinconsole/gate';
const gate = createGate({
routes: [
{ path: '/api/answer', price: '0.05', description: 'one research answer' },
{ path: '/api/premium/*', method: 'POST', price: '0.25' },
],
rails: facilitatorClientRails(facilitator), // or mockFacilitatorRails(...) offline
payTo: '0xYourTreasury…',
network: 'base-sepolia',
asset: USDC_ADDRESS,
screen: { denyPayers: ['0xKnownMule…'] },
});
app.use(gateMiddleware(gate)); // Express, or wrap any node:http handler
What the gate does that a bare 402 snippet doesn't:
- Quote consistency. A presented payment must match the gate's own quote — scheme, network, amount, recipient — before any facilitator round-trip. Underpayment is refused at the door.
- Payer screening. Allow/deny lists on the paying wallet — plus a dynamic
screen.checkhook (reputation plugs in here) — checked before verify/settle, so a blocked payer costs you nothing. - Replay protection. Each payment settles once; the slot is burned before the async legs, so two concurrent copies can't both pass (on-chain nonce burning is a luxury the mock chain doesn't have — the gate doesn't care).
- Receipts + revenue. Every settlement becomes a
GateReceipt(grc_ULID);gate.stats()aggregates revenue by asset, route, and payer;gate.quoted/gate.settled/gate.refusedevents stream on the bus. - Pluggable rails. The same gate runs against the mock facilitator (offline tests/demos) or the real hosted x402.org facilitator client — the rails are a two-method structural seam.
pnpm --filter @reinconsole/demo demo:gate # six scenarios, offline: guarded agent pays a gated vendor over real local HTTP
pnpm --filter @reinconsole/demo demo:sepolia-gate # the same gate on REAL rails: settles testnet USDC via the hosted facilitator
Graph: reputation closes the loop
Guard receipts say what agents tried to spend; gate receipts say what vendors actually earned. @reinconsole/graph (Phase 3) is the consumer of both — and the feedback path that turns observability into enforcement:
import { ReputationGraph, payerCheck } from '@reinconsole/graph';
const graph = new ReputationGraph().observe(engine).observe(indexer).observe(gate);
// Agent side: pushed scores make `vendorReputationLt` policies fire.
await graph.syncVendors(engine.spend);
// Vendor side: low-reputation wallets are turned away at the door.
createGate({ screen: { check: payerCheck(graph, { denyBelow: 40 }) }, ... });
- Evidence, not vibes. The graph accumulates per-subject history straight off the event buses: settlements, refusals (replays weighted heaviest), shadow spends, settled-money edges between counterparties, and manual dispute/endorsement reports. Scores are recomputed from raw evidence on demand —
GET /v1/scores/vendor/api.example.comreturns the score and everything behind it. - Five components + confidence. Settlement reliability, dispute hygiene, volume, longevity, and one-hop counterparty quality (who you settle with marks you), blended 0–100. Confidence is first-class: a thin or brand-new history yields low confidence, not a fake number.
- Unknown is not bad. The evaluator never fires
vendorReputationLtwithout data, the sync withholds low-confidence scores, andpayerCheckpasses wallets it knows nothing about. A newcomer is served; a confidently bad actor is refused. - The network effect. Evidence from one vendor's gate protects every other gate sharing the graph — a mule that replayed payments elsewhere is refused here, before any facilitator round-trip.
- One identity, one history.
graph.link(canonical, alias)merges subjects across id spaces — evidence recorded under either id folds together (counters sum, settled-money edges re-key on both ends), all future evidence and lookups resolve to the canonical identity, and merges persist on the durable store. Links are derived facts (your agent registry knows its wallets; ERC-8004 ids are the on-chain source): re-assert them at boot, idempotently. The console world does exactly this — one scoreboard row per party, andpayerCheckrefuses a wallet for what its agent did on the engine side.
pnpm --filter @reinconsole/demo demo:graph # five scenarios, offline: both feedback loops close live
ERC-8004: identity from the chain
@reinconsole/erc8004 makes the registry the source of link facts instead of local configuration. A registered agent becomes ERC-8004-canonical: its reputation row keys by eip155:{chainId}:{registry}/{tokenId}, and the engine ULID plus every wallet (ownerOf, the verified agentWallet) fold in as aliases — so two deployments claiming the same registration merge into one history, and key rotation never splits a score. Vendors stay host-canonical (hosts are what intents carry and vendorReputationLt matches); their identities and treasuries fold into the host row. Unregistered agents keep today's local linking — the fallback is byte-compatible.
pnpm --filter @reinconsole/demo demo:erc8004 # five scenarios, offline: one on-chain identity, one reputation
pnpm --filter @reinconsole/demo demo:sepolia-8004 # REAL registration on the Base Sepolia registry (one-time gas; re-runs read-only)
Run it as a service (buildGraphServer): remote producers POST /v1/events, anyone reads GET /v1/scores — or run the durable variant (services/store/dist/graph-server.js), where the evidence survives restarts (see Persistence). Or watch it live: the console world runs a graph over all four buses, re-syncs it into the engine after every burst of evidence, and renders the scoreboard with click-to-expand explanations.
The SDK one-liner
Wrap your agent's fetch, point it at a policy engine, and every x402 payment is governed:
import { createGuard } from '@reinconsole/sdk';
const guard = createGuard({
engineUrl: 'http://localhost:8787',
agentId: 'agt_01J...', // registered with the engine
onReceipt: (r) => console.log(r.outcome, r.amount, r.vendorHost),
});
const fetch = guard.wrap(); // a drop-in fetch
// A 402 from the vendor is intercepted, the intent is evaluated, and a
// blocked payment never reaches the network. Allowed payments flow through
// and the settlement is captured back onto the receipt.
const res = await fetch('https://api.vendor.example/v1/search?q=...');
The guard layers underneath any x402 payment library: the first unpaid request surfaces the 402, the guard evaluates it and either blocks it (so the payment layer never sees the paywall) or releases it upward — and the X-PAYMENT retry flows back through to attach the settlement. Or pass a payer and the guard settles directly.
Framework examples, each runnable against a free sandbox from npx @reinconsole/init: Vercel AI SDK (a governed tool()) and Coinbase AgentKit (an action provider that pays with the AgentKit wallet).
The policy engine API
POST /v1/evaluate is the hot path (sub-millisecond, signed + chained). Also: register agents, manage policies, flip the kill switch, and read the audit log.
| Method | Route | Purpose |
|---|---|---|
| GET | /health | Liveness + the engine's signing public key |
| POST | /v1/agents | Register an agent (returns a agt_ ULID) |
| GET | /v1/agents | List agents |
| POST | /v1/agents/:id/freeze | Kill switch on (deny everything) |
| POST | /v1/agents/:id/unfreeze | Kill switch off |
| POST | /v1/policies | Add a policy |
| GET | /v1/policies | List policies |
| POST | /v1/evaluate | Evaluate a payment intent → signed decision |
| GET | /v1/decisions | The hash-chained decision log |
| GET | /v1/chain/verify | The engine's verdict on its whole chain |
| GET | /v1/agents/:id/breakers | Where the agent's behavioral breakers stand |
| POST | /v1/settlements | Report that an allowed payment landed |
| GET | /v1/reconciliation | Allowances with no settlement behind them |
| PUT | /v1/agents/:id/liveness | Expect this agent to be active every interval |
| POST | /v1/agents/:id/heartbeat | "I am alive, I just have nothing to buy" |
| GET | /v1/liveness | Where every watched agent stands (worst first) |
A policy is declarative — for example, a $0.50 per-transaction cap plus a rolling $0.04/hour budget, defaulting to allow:
{
"policyId": "research-policy",
"appliesTo": { "agents": ["agt_01J..."] },
"rules": [
{ "id": "tx-cap", "deny": { "amountGt": "0.50" } },
{ "id": "hour-budget", "deny": { "rollingSum": { "window": "1h", "gt": "0.04" } } }
],
"default": "allow"
}
Breakers and task budgets
Rules ask about the payment in front of them. A breaker asks whether the agent's behavior has left the envelope it was given — and once it has, every subsequent intent escalates for a signed approval. It never denies on its own: a silent deny at the wrong moment strands a running job with no path forward and nobody told.
{
"policyId": "research-policy",
"breakers": [
{ "id": "velocity", "window": "1h", "txCount": 60, "valueCap": "5.00" }
],
"rules": [
{ "id": "task-cap", "escalate": { "taskBudget": { "gt": "1.00" } } },
{ "id": "untagged", "deny": { "taskIdMissing": true } }
],
"default": "allow"
}
- Tripwires are prospective and ORed: the payment that would carry the window past 60 transactions or past $5.00 is the one that escalates, rather than the innocent one behind it. At least one tripwire is required.
- A breaker sits at the escalate precedence level, so an explicit
denystill wins and noallowrule can wave a tripped breaker past. - It resets two ways, which are the same mechanism — a counting floor: the window
rolling forward, or a signed approval moving the floor to now.
GET /v1/agents/:id/breakers(orclient.breakerStates(agentId)) reports where each one stands. taskBudgetcaps cumulative spend on one unit of work rather than one window: the research run meant to cost a dollar cannot quietly cost fifty, however slowly. The guard'swithTask({ taskId })carries the attribution. An intent with no task id never triggers a budget — requiring attribution is the separate, deliberatetaskIdMissingrule, because otherwise every untagged probe payment would trip every task budget in the policy.
Reconciliation: allowed but never settled
An allow authorizes a payment; it does not make one. In between is a gap where a payment can quietly fail — a facilitator that never broadcast, a vendor that never confirmed, an agent that crashed mid-flight — and nothing in the stack notices on its own. The decision chain says "allowed", the rolling budget has already been charged, and the money simply never moved.
Rein closes that loop by joining the allowance ledger against settlement facts:
await client.reportSettlement({ intentId, txHash, source: 'indexer', confirmedAt: new Date() });
const report = await client.reconciliation({ window: '24h', graceMs: 60_000 });
// → { allowed, settled, inFlight, unsettled, unsettledValue, overspent, overspentValue, settlementsSeen, gaps: [...] }
The guard reports its own settlements automatically (fire-and-forget — a failed report
can never affect a payment that already succeeded); an indexer or facilitator webhook
is the stronger reporter, and reportSettlement: false hands the job over to it.
- A gap has an age, not a boolean. Under
graceMsa missing settlement is a payment in flight, which is the normal state of every payment for its first seconds. - The unsettled allowance keeps its charge against the budget. Refunding it would be a self-service reset: don't settle, and the envelope refills.
- The same join runs the other way: a settlement reported with an amount above the
amount allowed is an
overspentrow, listed first, withoverspentValuesumming the excess. Equal is right and less is inside the ceiling; only more is a breach. settlementsSeenis the honesty valve. The engine watches no chain — it is told when payments land — so zero reports means nobody is looking, and the gaps say more about the wiring than about the payments. The console renders that case differently.- Reconciliation is observability, never authority: it cannot deny, cannot alter a decision, and a settlement report authorizes nothing.
Dead man: the agent that simply stopped
Every control above answers "should this payment happen?". This one answers the question nothing else in the stack asks. An agent that dies raises no intent, breaks no budget and trips no breaker — it disappears, and a control plane watching only for bad payments reports a perfectly clean month while the work quietly stops.
await client.watchLiveness(agentId, { interval: '15m', note: 'polls the vendor feed' });
await client.heartbeat(agentId); // only for an agent with nothing to buy
const states = await client.liveness(); // → [{ status: 'missing', silentMs, ... }]
- An expectation is declared, never inferred. Most agents are episodic, so silence is only evidence about one somebody said should be periodic; an unwatched agent has no liveness state, and a heartbeat for one is refused rather than swallowed.
- Any intent is a sighting, including a denied one. An agent hammering a wall is alive — that is a different alarm, with a different remedy — so a spending agent needs no heartbeat at all.
- Silence has an age:
alive->late(inside the grace) ->missing. An alarm that cries at every wobble is one an operator learns to ignore, which loses the next agent. - The alarm fires once per silence, not once per sweep, and the bookkeeping is durable — a restart does not re-announce a death that has already been read.
- The engine never alarms about silence it did not witness. An engine that was down
cannot tell a dead agent from its own outage, so a silence older than the process reads
unknownuntil it has been up long enough to certify it. - Like reconciliation, it carries no authority: an alarm is news for a human, never an input to a decision. Alarms ride the same one-way channels as approvals — and there is nothing to acknowledge, because only the agent being seen again ends one.
Design principles
- Non-custodial. Rein governs authority to spend, not the funds.
- Fail closed. If the policy service is unreachable, payments above a configured floor are denied, not allowed. Ungovernable x402 offers fail closed too.
- Rail-agnostic core, x402-first integration. The policy/ledger domain model knows nothing about x402 specifically; x402 (Base, Solana) is the first adapter.
- Every decision is auditable. Each allow/deny produces a signed, hash-chained decision record linked to the eventual on-chain transaction.
- Honest about its tier. The facilitator — mock or the real hosted one — is not Rein-privileged: in SDK mode a rogue payment still settles, and the indexer flags it as shadow spend (verified live on Base Sepolia). The session-key signer tier closes that gap: the key the rogue would need no longer exists in the agent.
Repository layout
packages/
boot/ @reinconsole/boot — container boot: chown the data volume, drop root, before the DB opens [internal]
core/ @reinconsole/core — canonical zod schemas (single source of truth) [published]
sdk/ @reinconsole/sdk — agent-side guard; wraps the x402 client [published]
gate/ @reinconsole/gate — vendor-side x402 monetization middleware [published]
services/
policy-engine/ @reinconsole/policy-engine — Fastify policy evaluation service + audit log [published]
mock-rails/ @reinconsole/mock-rails — mock x402 facilitator + ledger + indexer [published]
x402-rails/ @reinconsole/x402-rails — real rails: EIP-3009 payer, x402.org facilitator client, on-chain indexer [published]
graph/ @reinconsole/graph — reputation: evidence off every bus, explainable scores, policy+gate feed [published]
erc8004/ @reinconsole/erc8004 — on-chain identity: ERC-8004 registry reads/writes → link facts [published]
signer/ @reinconsole/signer — session-key custody: voucher-gated EIP-3009 signing, caps, kill switch [published]
store/ @reinconsole/store — persistence: PGlite-backed engine + graph stores; state survives restarts [published]
apps/
demo/ @reinconsole/demo — end-to-end demos: mock (5 scenarios) + real Base Sepolia (guard + gate) + signer tier + gate + graph
console/ @reinconsole/console — live web UI: real-time feed, kill switch, audit chain, shadow-spend alerts
examples/
vercel-ai-sdk/ — spend limits for an AI SDK agent: a tool() that asks Rein before it pays
coinbase-agentkit/ — spend limits for an AgentKit agent: an action provider, the wallet signs
Tech
- Language: TypeScript end-to-end (Node 22 LTS), strict mode.
- Monorepo: pnpm workspaces + Turborepo.
- Schemas: zod, in
@reinconsole/core, as the single source of truth for DB rows, API payloads, and SDK types. - Build/test: tsup (esm + cjs + d.ts), vitest.
- Console: Vite + React + TypeScript, live updates over Server-Sent Events (no extra services to run).
- Chain: viem on Base Sepolia — EIP-712/EIP-3009 signing,
getLogsindexing, the hosted x402.org facilitator for settlement. - Dev mode: mock x402 flows + in-memory stores behind ports, with
@reinconsole/store(embedded PGlite Postgres) when you want state to survive restarts. No accounts or Docker required to run locally; hosted Postgres + Timescale + Redis + NATS wire in later behind the same ports.
Security
Found something that could move money, mint authority, or bypass a policy decision? Report
it privately through GitHub's advisory flow
rather than a public issue. SECURITY.md has the scope, the response times,
and the list of behavior that looks alarming but is deliberate — SDK mode is advisory and
bypassable by design, and knowing that saves everyone a round trip.
License
MIT