x402 turned HTTP 402 into a payment rail: agents pay per call, no cards, no invoices. But an agent transacts at machine speed — and a poisoned prompt or a runaway retry loop spends at machine speed too. “The agent will behave” is not a security model.
From npm install to a
governed agent payment.
Rein governs your agent's authority to spend — it never holds the funds. This runbook takes you from an empty folder to watching a policy engine allow four payments and refuse the fifth before any payment exists. Everything below was run, verbatim, against the published packages before this page shipped.
The quickstart runs 100% offline — no accounts, no chain, no funds. The live track at the end settles real testnet USDC.
Agents can spend money now.
Someone has to hold the reins.
- budgets & per-tx caps
- instant kill switch
- signed, hash-chained audit log
- price your API per call (x402)
- screen counterparties by reputation
- catch guard-bypassing spend
Guard the agent that spends, gate the API that earns, and let one reputation graph feed evidence back into enforcement on both sides.
- Non-custodial: authority moves through Rein, money doesn't
- Fail-closed: engine unreachable → payments deny, not allow
- Denies happen before a payment is constructed
- Offline dev loop: the whole stack runs with zero setup
- MIT, 8 packages on npm, live on Base Sepolia — with links, not adjectives
Two quickstarts. One prep.
Both quickstarts share the three prep commands below and run entirely offline against simulated rails — the same code paths, no real money.
GUARD
I'm building an agentCap what your agent can spend. Watch a budget deny fire before a cent moves.
guard quickstart →GATE
I'm selling an APIPrice your routes. An unpaid caller gets a quote; a paying one gets through — receipted.
gate quickstart →WATCH
I'm just lookingThe live console runs the whole two-sided story. Click Run scenario and watch.
app.reinconsole.com →Rein targets Node 22+. Any recent LTS works.
$ node -v
v22.14.0 # anything ≥22
Four packages: the agent-side guard, the vendor-side gate, the offline rails
they'll run on, and the core schemas (for newId).
$ mkdir rein-quickstart
$ cd rein-quickstart
$ npm init -y
$ npm install @reinconsole/sdk @reinconsole/gate @reinconsole/mock-rails @reinconsole/core # ~15 s
Open a new, second terminal window for this step — the engine stays running there for the rest of the runbook, and every other command goes in your first terminal. The engine is the referee: it holds your agents, policies, and the signed decision log, and the quickstarts talk to it on port 8787.
$ npx -p @reinconsole/policy-engine rein-policy-engine
[rein] policy-engine listening on http://0.0.0.0:8787
Put reins on an agent.
One file: register an agent, wrap its fetch, set a
$0.50 per-tx cap and a $0.04 rolling hourly budget — then watch call five get refused.
Copy this file into the rein-quickstart folder
from step 02, next to package.json — that's how
Node finds the packages you installed there. Then run it from that folder.
// guard-quickstart.mjs — cap an agent's spend, offline, in one file.
// Prereq: the policy engine is running: npx -p @reinconsole/policy-engine rein-policy-engine
import { newId } from '@reinconsole/core';
import { createGuard, EngineClient, PaymentBlockedError } from '@reinconsole/sdk';
import { MockLedger, MockFacilitator, createMockVendor } from '@reinconsole/mock-rails';
const engineUrl = 'http://127.0.0.1:8787';
const wallet = '0xResearch01';
// A simulated x402 world: a ledger, a settlement facilitator, and a
// paywalled vendor charging $0.01 per call. No accounts, no chain, no funds.
const ledger = new MockLedger();
const facilitator = new MockFacilitator({ ledger, name: 'mock-facilitator' });
const vendor = createMockVendor({ facilitator, atomicPrice: '10000', payTo: '0xVendorTreasury' });
// 1. Register the agent with the policy engine
const client = new EngineClient({ baseUrl: engineUrl });
const agent = await client.registerAgent({
orgId: newId('org'),
name: 'research-agent',
wallets: [{ chain: 'base', address: wallet, mode: 'sdk' }],
});
// 2. Wrap fetch with the guard
const guard = createGuard({
engineUrl,
agentId: agent.id,
fetch: vendor.fetch, // swap for globalThis.fetch against real vendors
payer: facilitator.payerFor(wallet),
});
// 3. Set the rules: max $0.50 per tx, max $0.04 per rolling hour
await guard.client.addPolicy({
policyId: 'starter-policy',
appliesTo: { agents: [agent.id] },
rules: [
{ id: 'tx-cap', deny: { amountGt: '0.50' } },
{ id: 'hour-budget', deny: { rollingSum: { window: '1h', gt: '0.04' } } },
],
default: 'allow',
});
// 4. Spend within budget — four $0.01 calls sail through
const guarded = guard.wrap();
for (let i = 1; i <= 4; i++) {
await guarded('https://api.data.test/v1/query');
const r = guard.receipts().at(-1);
console.log(`call ${i} ALLOW $0.0${i}/$0.04 spent · tx ${r.settlement?.txHash?.slice(0, 12)}…`);
}
// 5. The fifth call would breach the budget — denied BEFORE any payment exists
try {
await guarded('https://api.data.test/v1/query');
} catch (e) {
if (!(e instanceof PaymentBlockedError)) throw e;
console.log(`call 5 DENY ${e.decision.reason}`);
console.log(`ledger has ${ledger.entries().length} entries — the denied payment was never constructed`);
}
$ node guard-quickstart.mjs
call 1 ALLOW $0.01/$0.04 spent · tx 0x5ab6460ff8…
call 2 ALLOW $0.02/$0.04 spent · tx 0xa8725d31cb…
call 3 ALLOW $0.03/$0.04 spent · tx 0x5de05ac3a4…
call 4 ALLOW $0.04/$0.04 spent · tx 0x565141762f…
call 5 DENY denied by: hour-budget
ledger has 4 entries — the denied payment was never constructed
That last line is the whole product: the deny happened at intent time — no
payment was signed, sent, or settled. The engine also recorded every decision in an
ed25519-signed, sha256-chained audit log. Point
fetch: globalThis.fetch at a real x402 vendor and the
same policy loop governs real payments.
Put a paywall on an API.
The vendor side is two calls: createGate to price your
routes, gateMiddleware in front of your handler. This file
does both — then buys through its own paywall with a guarded agent, so you see both sides
of one payment. Keep the engine from step 03 running.
Same place as before: inside rein-quickstart,
next to guard-quickstart.mjs.
// gate-quickstart.mjs — put a paywall on any Node API, offline, in one file.
// Prereq: the policy engine is running: npx -p @reinconsole/policy-engine rein-policy-engine
import { createServer } from 'node:http';
import { newId } from '@reinconsole/core';
import { createGuard, EngineClient } from '@reinconsole/sdk';
import { createGate, gateMiddleware, mockFacilitatorRails } from '@reinconsole/gate';
import { MockLedger, MockFacilitator } from '@reinconsole/mock-rails';
// A simulated settlement network (swap for facilitatorClientRails(...) on real rails)
const ledger = new MockLedger();
const facilitator = new MockFacilitator({ ledger, name: 'mock-facilitator' });
// 1. Price your routes
const gate = createGate({
routes: [
{ path: '/api/answer', price: '0.05', description: 'one research answer' },
{ path: '/api/premium/*', method: 'POST', price: '0.25' },
],
rails: mockFacilitatorRails(facilitator),
payTo: '0xVendorTreasury',
network: 'base',
asset: 'USDC',
});
// 2. Put the middleware in front of your handler (Express: app.use(gateMiddleware(gate)))
const paywall = gateMiddleware(gate);
const server = createServer((req, res) => {
paywall(req, res, () => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ answer: 42 }));
});
});
await new Promise((r) => server.listen(8402, '127.0.0.1', r));
// 3. An unpaid caller gets a machine-readable 402 quote, not a dead end
const unpaid = await fetch('http://127.0.0.1:8402/api/answer');
const quote = await unpaid.json();
console.log(`unpaid HTTP ${unpaid.status} — quote: ${quote.accepts[0].maxAmountRequired} atomic USDC to ${quote.accepts[0].payTo}`);
// 4. A guarded agent buys through the paywall — Rein on both sides of the wire
const engineUrl = 'http://127.0.0.1:8787';
const wallet = '0xBuyerWallet01';
const client = new EngineClient({ baseUrl: engineUrl });
const agent = await client.registerAgent({
orgId: newId('org'),
name: 'buyer-agent',
wallets: [{ chain: 'base', address: wallet, mode: 'sdk' }],
});
const guard = createGuard({
engineUrl,
agentId: agent.id,
payer: facilitator.payerFor(wallet),
fetch: (input, init) => globalThis.fetch(input, init),
});
await guard.client.addPolicy({
policyId: 'buyer-policy',
appliesTo: { agents: [agent.id] },
rules: [{ id: 'tx-cap', deny: { amountGt: '1.00' } }],
default: 'allow',
});
const paid = await guard.wrap()('http://127.0.0.1:8402/api/answer');
console.log(`paid HTTP ${paid.status} · ${JSON.stringify(await paid.json())}`);
// One payment, receipts on BOTH sides that agree
const buyerReceipt = guard.receipts().at(-1);
const gateReceipt = gate.receipts.at(-1);
console.log(`buyer receipt ${buyerReceipt.id.slice(0, 16)}… · $${buyerReceipt.amount} · tx ${buyerReceipt.settlement?.txHash?.slice(0, 12)}…`);
console.log(`gate receipt ${gateReceipt.id.slice(0, 16)}… · $${gateReceipt.amount} · tx ${gateReceipt.transaction?.slice(0, 12)}…`);
server.close();
$ node gate-quickstart.mjs
unpaid HTTP 402 — quote: 50000 atomic USDC to 0xVendorTreasury
paid HTTP 200 · {"answer":42}
buyer receipt rcp_01KX3SM05MS3… · $0.05 · tx 0xa650338ff1…
gate receipt grc_01KX3SM05GDR… · $0.05 · tx 0xa650338ff1…
Same transaction hash on both receipts: the buyer's guard and the vendor's gate each
produced an independent record of one payment — and they agree. In production the
gate also screens payers, rate-limits, burns replayed payments at the door, and
enforces per-payer velocity caps; swap
mockFacilitatorRails for
facilitatorClientRails to settle through the hosted
x402.org facilitator (free, no API key).
Five failure modes, one run,
under a second.
The repo ships demos that exercise everything the quickstarts didn't — offline, with a 435-test suite behind them.
$ git clone https://github.com/bugiiiii11/rein
$ cd rein
$ npm install -g pnpm # skip if `pnpm -v` works · corepack enable is the alternative (needs an admin shell on Windows)
$ pnpm install # ~40 s
$ pnpm build # ~2 min · 12 packages
$ node apps/demo/dist/index.js
- no. 1 ALLOW $0.01 ×4 normal calls, within budget settled + receipted
- no. 2 DENY $0.01 → rolling budget would break before payment exists
- no. 3 DENY $5.00 → over the per-tx cap before payment exists
- no. 4 FROZEN $0.01 → kill switch engaged, then released deny → allow
- no. 5 SHADOW $2.50 → guard bypassed — indexer catches it flagged, unreconciled
Then keep going: signer.js — six rogue paths, every one refused · gate.js — the vendor side over real local HTTP · graph.js — reputation closing the loop on both sides · pnpm test — the full 435 (~3 min).
The same loop, real chain,
free testnet money.
Everything above, but the settlement is a real USDC transfer on Base Sepolia through the hosted x402.org facilitator. Costs nothing: the USDC is from a faucet and the facilitator pays the gas. You'll need the repo clone from the section above.
The first run generates an agent wallet into .env
and prints faucet instructions.
$ pnpm --filter @reinconsole/demo demo:sepolia
Grab testnet USDC for the printed address at faucet.circle.com (pick Base Sepolia). No ETH needed — the facilitator pays gas on this path.
A full run spends $0.02 of testnet USDC: one guarded payment the policy engine
allowed (settled on-chain, reconciled to the exact intent via a
keccak256(intent.id) nonce memo), and one rogue
payment that bypasses the guard — settled anyway, then flagged as a shadow spend.
It ends with two BaseScan links like these, from our runs:
- the settled guarded payment — real USDC, policy-approved
- the shadow spend, caught — unguarded transfer, flagged
Then the vendor side on the same rails —
pnpm --filter @reinconsole/demo demo:sepolia-gate
settles a gate-priced payment through the hosted facilitator
(a real one)
and burns a replayed copy at the door.
ERC-8004 registers your agent as an NFT on a singleton registry; Rein then publishes
the reputation graph's score to the on-chain Reputation Registry. Try it offline
first (demo:erc8004), then for real — this one needs
a little Base Sepolia ETH for gas; re-runs are read-only.
$ pnpm --filter @reinconsole/demo demo:sepolia-8004
Ours is agent #7393, carrying a 82/100 rein-score published on-chain.
NODE_EXTRA_CA_CERTS to
your CA bundle — details in env.example. Offline
quickstarts and demos are unaffected.
What Rein is — and isn't. Yet.
- Non-custodial by architecture — funds never pass through Rein
- Fail-closed — an unreachable engine denies, it doesn't shrug
- Open source, MIT, 435 offline tests, live on Base Sepolia
- Two-sided: the same stack guards buyers and gates sellers
- Auditable — every decision ed25519-signed and hash-chained
- A wallet or custodian — it never holds keys to your funds
- Hard enforcement in SDK mode — an agent holding its own key can bypass the guard; Rein catches that (shadow spend). Key-less enforcement is the session-signer tier, in the repo today, published at GA
- 1.0 — this is v0.1; APIs may change
- Mainnet-proven — everything on-chain so far is Base Sepolia testnet
Now put reins on yours.
$ npm install @reinconsole/sdk
v0.1 — early open-source infrastructure, live on testnet. APIs may change before 1.0.
No waitlist. No email capture. The code is public.