1. Install, and check what you installed
In an empty folder:
npm init -y
npm install @reinconsole/sdk @reinconsole/store @reinconsole/mock-rails @reinconsole/core
npm audit signatures
audited 73 packages in 7s
73 packages have verified registry signatures
26 packages have verified attestations
The attestations are npm provenance: each @reinconsole package was built and
published by the release workflow
in the public repo, from a tagged commit, with no npm token in the loop. Each package's npm page links the
exact commit.
2. Run the engine on your own disk
In a second terminal, in the same folder. This is rein-engine from
@reinconsole/store, the persistent engine the hosted service runs. It keeps
agents, policies, its signing key and the decision log in ./rein-data.
REIN_DATA_DIR=./rein-data npx -p @reinconsole/store rein-engine
# PowerShell: $env:REIN_DATA_DIR = './rein-data'; npx -p @reinconsole/store rein-engine
[rein] no REIN_ENGINE_API_KEY set — binding 127.0.0.1 only. Set one before exposing this engine.
[rein] persistent policy-engine listening on http://127.0.0.1:8787 (auth: none)
[rein] data dir ./rein-data — fresh store; signing key stored
With no API key set it binds to localhost only, so nothing else on the network can reach it. The ed25519 signing key is generated here and stays in that folder.
3. Govern five payments
One agent, a $1.00 per-payment cap and the kill switch, on simulated x402 rails, so no money and no chain are
involved. Save as govern.mjs in the same folder:
// govern.mjs — five payment attempts against YOUR engine, on simulated rails.
// Prereq: the engine from step 03 is running on 127.0.0.1:8787.
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 = '0xLocalAgent01';
const ledger = new MockLedger();
const facilitator = new MockFacilitator({ ledger, name: 'mock-facilitator' });
const cheap = createMockVendor({ facilitator, atomicPrice: '10000', payTo: '0xVendor' }); // $0.01
const pricey = createMockVendor({ facilitator, atomicPrice: '5000000', payTo: '0xVendor' }); // $5.00
const client = new EngineClient({ baseUrl: engineUrl });
const agent = await client.registerAgent({
orgId: newId('org'),
name: 'local-agent',
wallets: [{ chain: 'base', address: wallet, mode: 'sdk' }],
});
await client.addPolicy({
policyId: `local-${agent.id}`,
appliesTo: { agents: [agent.id] },
rules: [{ id: 'tx-cap', deny: { amountGt: '1.00' } }],
default: 'allow',
});
const pay = (vendor) =>
createGuard({ engineUrl, agentId: agent.id, fetch: vendor.fetch, payer: facilitator.payerFor(wallet) }).wrap();
async function attempt(label, vendor) {
try {
await pay(vendor)('https://api.data.test/v1/query');
console.log(`${label} ALLOW`);
} catch (e) {
if (!(e instanceof PaymentBlockedError)) throw e;
console.log(`${label} DENY ${e.decision.reason}`);
}
}
await attempt('$0.01', cheap);
await attempt('$0.01', cheap);
await attempt('$5.00', pricey); // over the $1.00 per-tx cap
await client.freeze(agent.id); // the kill switch
await attempt('$0.01', cheap);
await client.unfreeze(agent.id);
await attempt('$0.01', cheap);
console.log(`ledger: ${ledger.entries().length} payments settled, 2 never constructed`);
node govern.mjs
$0.01 ALLOW
$0.01 ALLOW
$5.00 DENY denied by: tx-cap
$0.01 DENY agent is frozen (kill switch)
$0.01 ALLOW
ledger: 3 payments settled, 2 never constructed
4. Verify the log without our code
Every decision is sha256-hashed over its content plus the previous hash, then ed25519-signed. This script
fetches the engine's public key and the whole log and checks both, with node:crypto
and nothing else. Save as verify.mjs:
// verify.mjs — check the engine's decision log with node:crypto alone.
// No Rein package is imported: this file trusts nothing we wrote.
import { createHash, createPublicKey, verify } from 'node:crypto';
const engine = process.env.REIN_ENGINE_URL ?? 'http://127.0.0.1:8787';
const { publicKey } = await (await fetch(`${engine}/health`)).json();
const key = createPublicKey(publicKey);
const log = [];
for (let after; ; ) {
const res = await fetch(`${engine}/v1/decisions?limit=500${after === undefined ? '' : `&after=${after}`}`);
log.push(...(await res.json()));
after = res.headers.get('rein-next-after') ?? undefined;
if (after === undefined) break;
}
// --tamper rewrites the $5.00 deny as an allow, the edit an attacker would want.
if (process.argv.includes('--tamper')) log.find((d) => d.outcome === 'deny').outcome = 'allow';
let prev = 'genesis';
for (const [i, d] of log.entries()) {
// The exact bytes the engine hashed: these eight fields, in this order.
const canonical = JSON.stringify({
intentId: d.intentId, intentHash: d.intentHash, outcome: d.outcome,
matchedRules: d.matchedRules, policyId: d.policyId, policyVersion: d.policyVersion,
prevHash: d.prevHash, decidedAt: new Date(d.decidedAt).toISOString(),
});
const hash = createHash('sha256').update(canonical).digest('hex');
const signed = verify(null, Buffer.from(d.hash), key, Buffer.from(d.signature, 'base64'));
if (d.prevHash !== prev || hash !== d.hash || !signed) {
console.log(`BROKEN at #${i} (${d.id}): ${d.prevHash !== prev ? 'chain' : hash !== d.hash ? 'hash' : 'signature'}`);
process.exit(1);
}
prev = d.hash;
}
console.log(`${log.length} decisions · chain intact · every signature valid · head ${prev.slice(0, 12)}…`);
node verify.mjs
node verify.mjs --tamper
5 decisions · chain intact · every signature valid · head 3376c09a8532…
BROKEN at #2 (dec_01M3QBKVRHSWBNMZM36S66W4AP): hash
The second run rewrites the $5.00 refusal as an allow before checking, and the check fails at that entry. An edit that also recomputed the hash would fail the signature instead, and a dropped entry breaks the chain.
5. Restart it: the record stays
Stop the engine (Ctrl+C), start it again with the same command, and run both scripts once more. The engine resumes from disk and the new decisions extend the same chain:
[rein] data dir ./rein-data — resumed 5 decisions, 1 agents, 1 policies, 0 api keys; signing key stored
...
10 decisions · chain intact · every signature valid · head 29403df62d6a…
What leaves your machine
- The install: npm downloads the packages. That is the only network traffic on this page.
- The engine: nothing. Its only outbound call is to the Telegram Bot API, and only if you set
REIN_TELEGRAM_BOT_TOKENandREIN_TELEGRAM_CHAT_IDfor approvals. - Your keys: Rein never asks for a wallet key. The guard decides before a payment is signed; your own payer signs it.
From here to real payments
Swap the mock vendor's fetch for globalThis.fetch and the mock payer for your
own, and the same engine governs real x402 payments. The
runbook has a Base Sepolia track with free testnet USDC. Before
exposing the engine beyond localhost, set REIN_ENGINE_API_KEY; the
deploy guide
covers the rest.