A working demo and product foundation built on the x402 protocol on Solana devnet, implementing all 6 layers of the Agent Payments Stack.
OpsRail starts from the x402 Solana sample and extends it into a broader operations product for AI-driven payments, execution safety, and finance workflows.
Status — proof-of-concept / demo, not production. On-chain settlement is simulated (a mock transaction; real finality, retries, and idempotent settlement are not built). See
docs/opsrail-framing.md→ "Honest current scope" for the full list of what is and isn't real. The end-to-end Agent Safety Stack demo — a trustless cross-VM agent payment (Probatio trust × OpsRail control × Veil privacy) on a QuickNode-native x402 rail — runs network-free vianpm run demo:agent-safety; seedocs/agent-safety-stack-demo.md.Licensed under Apache-2.0 (see
LICENSE/NOTICE); derived from the x402 sample (Apache-2.0).
The control plane is implemented across all three product tracks and driven from a purpose-built operator
console (Vite + React) at /. The operator surface is now protected by a minimal session-based auth
layer (viewer / operator / approver / admin), and any recorded operator action derives its actor from the
authenticated server session rather than a client-supplied string. Multiple payout surfaces run the same spine — one-shot remittance,
recurring payroll/AP, invoice-linked AP, and a lightweight keeper — through the same x402-paid gates, the
same reliability preflight, the same M-of-N approval loop, and the same audit trail. Every operator view
(decision feed, approvals, run explorer, budgets, treasury) is reconstructed from that one trail. For a
scripted walkthrough see docs/opsrail-demo-script.md; the original
sample-derived pitch demo is preserved at /classic/.
This repository is the starting point for OpsRail.
OpsRail is the control plane between an AI agent and money movement on Solana: it decides what executes automatically, what pauses for a human, and records every step.
Before any spend or settlement, OpsRail runs three gates and writes every decision to an append-only audit trail:
- Policy — is this spend authorized? (budget, per-tx limit, country allowlist)
- Compliance — is the counterparty allowed? (sanctions / PEP screening)
- Reliability preflight — is auto-execution safe, or does a human decide? (
ok/retryable/manual_review)
Safe actions execute autonomously; risky ones pause for approval. It turns "an agent that can spend" into "an agent you can let spend."
The three product tracks are the same control plane sold to three buyers — a wedge, an enabler, and an expansion, not three peers:
Agent Treasury— the wedge. Delegated budgets and policy-bound autonomous agent spend.Reliability Copilot— the enabler. The trust layer inside the wedge: preflight classification and safer execution that make autonomous spend acceptable.Payroll / AP— the expansion. The same gates + audit applied to recurring payouts, invoice-linked approvals, department budgets, and finance ops.
The current x402-derived demo proves the gates + audit spine end-to-end across all three tracks —
a visible manual_review pause before settlement, a pending-approvals queue with M-of-N multi-approver
resolution, department-budget gating, and an audit-derived treasury report — all reconstructed from the
audit trail. See docs/opsrail-framing.md for the full product framing,
including the milestone note now that all three tracks are implemented and cross-reviewed.
This repository is meant to be advanced collaboratively with both Claude Code and Codex.
- The collaboration model follows a
probatio-style pattern: cross-review first, with task ownership split by frame rather than permanent module boundaries. - Frame-thin work tends to start with
Claude Code; frame-thick work tends to converge withCodex. - Whoever implements a change is not its final reviewer.
Shared guidance lives in AGENTS.md.
| Layer | Role | This Demo |
|---|---|---|
| L0 — Settlement | Where transactions land | Solana devnet + USDC (SPL Token) |
| L1 — Wallet | Agent key management | @solana/kit Ed25519 keypair signer |
| L2 — Routing | Cross-rail optimization | Multi-corridor: USDC/M-Pesa vs BVNK vs SWIFT vs Western Union |
| L3 — Protocol | How the agent pays | x402 — HTTP 402 micropayments (@x402/express + @x402/fetch) |
| L4 — Governance | Spending authorization | Policy engine: budget cap ($0.01), per-tx limit ($0.005), country allowlist |
| L5 — Application | What the agent does | Cross-border remittance: FX, compliance/PEP screening, routing, settlement |
The console's Config & Launcher screen (and the legacy /classic/ demo) can launch these scenarios —
see docs/opsrail-demo-script.md for the scripted walkthrough:
| Scenario | Amount | Recipient | Result |
|---|---|---|---|
| Normal transfer | $500 | Mwangi Construction Ltd | Success — all 6 layers pass, saves $40 vs SWIFT |
| High-value transfer | $10,000 | Mwangi Construction Ltd | Paused by Reliability Copilot — preflight flags it for manual review before settlement |
| Large transfer | $50,000 | Mwangi Construction Ltd | Blocked at L4 — enhanced due diligence fee ($0.006) exceeds per-tx limit ($0.005) |
| Flagged recipient | $500 | Ministry of Defence Kenya | Blocked at L5 — compliance screening detects government/military entity (PEP) |
| Recurring payroll batch | 3 cycle-runs | 3 payees / schedules | 1 auto-complete + 2 approval holds — same spine reused for known-payee, new-payee, and over-allowance cycles |
| Invoice-linked AP run | 2 invoices + 1 duplicate attempt | 2 vendors / obligations | 1 auto-pay + 1 approval hold + 1 duplicate refused — durable invoice object, audit-derived paid state, same approval seam |
| Keeper tick | Current due cycles | active schedules | Due cycles instantiate once — current cadence occurrence only, no silent backfill, no duplicate second tick |
┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ AI Agent │ GET │ API Server │ │ x402.org │
│ (L1 wallet) │────────▶│ (L3 x402) │ │ Facilitator │
│ │ │ │ │ │
│ L4 policy │◀────────│ 402 + payment │ │ │
│ checks each │ │ requirements │ │ │
│ API call │ │ │ │ │
│ │────────▶│ X-PAYMENT hdr │────────▶│ settles │
│ L5 app logic │ │ │ │ on-chain │
│ decides next │◀────────│ 200 + data │◀────────│ (L0) │
│ step │ │ │ │ │
└─────────────────┘ └─────────────────┘ └──────────────┘
│
┌─────────┴─────────┐
│ L2 Routing │
│ L5 FX / Comply │
│ (paid endpoints) │
└───────────────────┘
| Endpoint | Price | Layer | Description |
|---|---|---|---|
GET /api/free |
Free | — | Health check |
GET /api/fx-rate?pair=USD/KES |
$0.001 | L5 | FX rates (USD/KES, USD/NGN, USD/GHS, USD/TZS, USD/ZAR, JPY/USD, EUR/USD) |
GET /api/compliance?country=KE&name=... |
$0.001 | L5 | Sanctions + PEP screening (OFAC, UN, EU, PEP Registry) |
GET /api/route?amount=500&dest=KE |
$0.002 | L2 | Optimal payment corridor (4 routes compared) |
GET /api/settle?amount=500&dest=KE&route=... |
$0.001 | L0 | Execute settlement on Solana |
GET /api/audit?runId=... |
Free | OpsRail | Read the durable audit trail for demo and product events |
GET /api/runs |
Free | OpsRail | Read the run-level decision feed derived from the audit trail |
GET /api/runs/:runId |
Free | OpsRail | Read a selected run summary plus ordered audit events |
POST /api/runs/:runId/release |
Free | OpsRail | Release a stale started-only reservation so the payment identity can be retried safely |
GET /api/approvals |
Free | OpsRail | Read the pending-approvals queue derived from the audit trail |
POST /api/approvals/:runId/approve |
Free | OpsRail | Resume a paused run and continue settlement |
POST /api/approvals/:runId/reject |
Free | OpsRail | Decline a paused run and close it |
GET /api/invoices |
Free | OpsRail | Read the durable invoice registry with audit-derived payment state |
POST /api/invoices |
Free | OpsRail | Define a durable invoice obligation linked to a payee |
POST /api/invoices/:invoiceId/pay |
Free | OpsRail | Instantiate an invoice payment as a normal run on the existing spine |
GET /api/payroll/schedules |
Free | OpsRail | List recurring schedules, payees, and current-period history |
POST /api/payroll/payees |
Free | OpsRail | Define a durable payable entity for recurring payouts |
POST /api/payroll/schedules |
Free | OpsRail | Define a recurring payout schedule with allowance |
POST /api/payroll/schedules/:scheduleId/run |
Free | OpsRail | Instantiate one payout cycle as a normal run on the existing spine |
| `GET /api/reports/treasury?groupBy=period&format=json | csv` | Free | OpsRail |
GET /api/keeper/status |
Free | OpsRail | Inspect in-process keeper status, due count, and surfaced missed cycles |
POST /api/keeper/tick |
Free | OpsRail | Manually trigger one keeper tick for demo / operator observation |
The default surface is the rebuilt operator console (Vite + React), served at GET / (which
redirects to /console/). It is a purpose-built, workflow-shaped console with six screens, each
reconstructed from the audit trail:
- Decision Feed — live run stream across all surfaces, color-coded by control-plane state
- Approvals Inbox — paused runs with plain-language reasons, M-of-N multi-approver backed by authenticated approver identity, approve / reject
- Run Explorer — a run's ordered audit-event timeline (the evidence / "why it paused")
- Budgets & Departments — allocation vs committed (
settled + reserved), over-allocation, linked payees - Treasury & Reports — settled / reserved / released / blocked rollup by period / department / payee / surface, with spreadsheet-safe CSV / JSON export
- Config & Launcher — create departments / payees / schedules / invoices, keeper status/tick, and buttons that launch the demo scenarios
For a scripted walkthrough (the ~2.5-minute winning run), see
docs/opsrail-demo-script.md.
Legacy pitch demo: the original sample-derived 4-page narrative (The Problem / The Solution / Architecture / Live Demo, EN/JA) is preserved at
GET /classic/as a rollback-safe path and a story-first pitch surface. It is no longer the default.
Authentication posture in this branch:
GET /loginserves the console login shellGET /console/andGET /classic/redirect unauthenticated users to login- read APIs require
viewer+ - config / launcher / keeper mutation routes require
operator+ - approval / rejection / reservation release require
approver+ - approval identity is server-derived from the authenticated session, so one person cannot satisfy a 2-of-N rule by spoofing
req.body.actor
Honest scope: auth gates the operator surface and the audit-derived APIs, but per-field PII redaction, IdP/SSO, MFA, and brute-force protection remain future work.
The console consumes the existing API/SSE only (no backend logic lives in the UI); the reliability
preflight still distinguishes ok / retryable / manual_review before settlement, and the operator
loop (decision feed, approvals, explorer, recovery, resume) is exactly the audit-derived control plane
described below.
Recurring payouts are modeled as:
- durable payees and schedules stored locally
- on-demand cycle instantiation with a fresh
runIdper cycle - recurring-specific approval reasons layered onto the existing
manual_reviewseam - cadence-aware cycle identity (
cycleKey) distinct from the monthly allowance window (periodKey)
Invoice-linked AP is modeled as:
- a durable invoice registry linked to existing payees
- audit-derived payment state (
open/in_flight/paid/void) rather than a mutable paid flag - same-
invoiceIdduplicate prevention via the same reservation discipline used for keeper cycle dedup - invoice-specific approval reasons layered onto the existing
manual_reviewseam
The keeper layer now adds a lightweight time-instantiation path:
- an optional in-process timer, gated by
OPSRAIL_KEEPER_ENABLED=true - current-cycle-only instantiation for
activeschedules - additive provenance on
run.startedviatrigger: "keeper" | "operator" - no silent backfill after downtime: missed cycles are surfaced, not auto-paid
- demo-safe visibility through
POST /api/keeper/tickand the keeper status panel
The control plane now keeps a lightweight durable audit log by default:
- persisted to
.opsrail/audit-log.jsonlunlessOPSRAIL_AUDIT_FILEoverrides it - bounded by
OPSRAIL_AUDIT_MAX_EVENTSwith a default retention of500events - replayed on boot so the decision feed, pending approvals, and resumable paused runs survive restart
- persisted with data minimization: raw API bodies are not kept wholesale, and recipient fields are explicitly tagged as PII
Treasury reporting is now modeled as a pure projection:
- every rollup is rebuilt from
auditStore.list()at read time - one canonical run classifier splits payout principal into
settled/reserved/released/blocked - grouping is one dimension at a time in v1:
period,department,payee, orsurface - JSON and CSV exports are available from the same endpoint, with CSV escaping and spreadsheet-formula neutralization
Reservation recovery is now modeled explicitly:
run.startedremains the reservation point for duplicate prevention- a started-only run that never reaches
run.pausedorrun.completedbecomes operator-visible asrecoverable - operator release appends a terminal
run.completedfailure withrecoveryAction: "stale_reservation_released" - keeper cycle dedup and invoice same-id dedup both honor that released state on replay
# Install dependencies
npm install
# Generate Solana keypairs
npm run keygen
# Copy output to .env (see .env.example)
# Create a git-ignored operator registry with hashed passwords at .opsrail/operators.json
# (or point OPSRAIL_OPERATORS_FILE at another local path)
# Fund client wallet with devnet SOL + USDC
solana airdrop 2 <CLIENT_WALLET_ADDRESS> --url devnet
# Get devnet USDC from https://faucet.circle.com/
# Start the server (serves the API + the pre-built operator console)
npm run server
# Open http://localhost:4021 → redirects to the authenticated operator console at /console/Note: The demo works even without devnet USDC — it falls back to internal API calls while still showing the full x402 flow in the UI.
Console note: the operator console is pre-built into
public/console/, sonpm run serverserves it as-is — no build step needed to run the demo. To develop the console, usenpm run console:dev(Vite dev server); to rebuild the committed assets,npm run console:build. (Vite requires Node 20+.) The legacy pitch demo stays at/classic/.
Clean demo state: stop the server,
rm -rf .opsrail, then restart to reset the audit log / registries to fixtures (recommended before a scripted run — see the demo script).
Persistence note: server runs now persist the audit log locally. Use
OPSRAIL_AUDIT_FILEto change the file path andOPSRAIL_AUDIT_MAX_EVENTSto change the retention bound.
Keeper note: set
OPSRAIL_KEEPER_ENABLED=trueto run the in-process timer automatically. The demo UI also exposes atick nowpath so keeper behavior is observable without waiting for real cadence boundaries.
opsrail/
├── console/ # Operator console source (Vite + React + TS) — the default UI
│ └── src/
├── public/
│ ├── console/ # Built console assets, served at /console/ (and default / redirects here)
│ └── classic/ # Legacy sample-derived 4-page pitch demo, served at /classic/
├── src/
│ ├── server.ts # Express server: x402 middleware, paid APIs, OpsRail control-plane APIs, SSE flows, / -> /console redirect
│ ├── lib/ # audit / policy / reliability / approvals / reservations / payroll / invoices / keeper / departments / reporting (+ tests)
│ ├── client.ts # CLI agent (standalone x402 payment client)
│ └── keygen.ts # Solana keypair generator
├── docs/ # task briefs, framing, roadmap, demo script
├── reviews/ # cross-review log
├── .env.example
├── package.json # server (tsx) + console (vite) scripts
└── tsconfig.json
This is a PoC demo. The following gaps exist between this implementation and a production-grade agent payment system.
The current policy engine uses fixed values (budget cap, per-tx limit, country allowlist). Production requires:
- Dynamic policy engine — Authorization logic that factors in transaction history, destination trust scores, and market volatility (e.g., FX rate spikes increasing corridor fees beyond the budget mid-flow)
- Intent monitoring — The current per-tx limit cannot prevent "salami attacks" where a compromised agent drains assets through thousands of individually-valid micro-transfers. A sliding-window anomaly detector is needed alongside budget caps
- Rate limiting on paid APIs — An attacker can force the agent to exhaust its budget by repeatedly triggering expensive compliance checks (e.g., submitting PEP-flagged entity names). Cached results and per-endpoint rate limits would mitigate this
The demo assumes settlement always succeeds. Production requires:
- Error recovery state machine — Defined refund processes for L0 settlement failures, retry logic with exponential backoff for L3 paid API calls, and idempotency keys to prevent double-settlement
- Finality awareness — The fallback mode ("works without devnet USDC") masks real on-chain behaviors like finality delays and transaction failures that must be handled explicitly
The current persistence layer is intentionally lightweight: a local JSONL audit log plus replay-based rehydration of paused runs. It is enough to survive a single-process restart and keep the demo honest, but production still requires:
- Durable multi-node storage — Shared persistence instead of a single local file
- Authenticated operator access — The decision feed and approval APIs are still unauthenticated demo surfaces
- PII redaction/export policy — We now minimize what persists, but not every consumer-facing view is redacted for production use
- Treasury reports cover the retained audit window, not all history — Run-aware retention can trim old completed runs, so past periods eventually need snapshot/archive support for long-horizon finance reporting
Task 008 adds a lightweight in-process keeper, but it is intentionally conservative. It only instantiates the current due cycle, never bursts historical payouts after downtime, and uses the audit trail as the source of truth for duplicate suppression. Production still requires:
- Multi-node scheduling semantics — No leader election or distributed coordination yet
- Richer cadence / timezone handling — The demo uses a small UTC cadence model (
daily/weekly/monthly) - Explicit backfill tooling — Missed cycles are surfaced, not auto-paid; any catch-up flow would need operator-visible policy
- Department budgets are still flat monthly allocations — Department spend now includes reserved commitments from the audit trail, but hierarchy and non-monthly allocations are still deferred
- Approval identity is still actor-string best effort — Multi-approver can dedupe by
actor, but true separation-of-duties still needs authenticated operator identity - Treasury reporting is payout-principal only — x402 API micro-fee reporting, multi-currency normalization, and GL/ERP formats are still deferred
- Pay-in-full only for invoices — Invoice-linked AP currently treats one successful run as the full settlement of one invoice; partial payments and installment ledgers are not modeled yet
- Invoice / vendor system integration — No OCR, invoice matching, or ERP connectivity yet
The demo stores the agent's private key as a base58 string in .env, loaded into process memory. Production requires:
- TEE (Trusted Execution Environment) — Manage private keys within hardware-isolated enclaves (e.g., AWS Nitro Enclaves) with governance policies bound to the key, so the key cannot be used outside approved policy contexts
- MPC or smart contract wallets — Multi-party computation or on-chain policy enforcement (e.g., Safe) as alternatives to single-keypair signing
- x402 Facilitator trust model — The demo relies on
x402.org/facilitatoras a trusted third party. The trust assumptions and decentralization roadmap of the Facilitator are defined by the x402 protocol, not this demo - Replay attack resistance — Handled by x402's
SettlementCache(120s TTL) at the protocol level. Not demonstrated in the UI but present in the middleware - Regulatory compliance (VASP) — The demo performs PEP screening at L5, but the question of who bears legal liability for an AI agent's transactions remains an open regulatory question industry-wide
- x402:
@x402/core,@x402/svm,@x402/express,@x402/fetch - Solana:
@solana/kit(v5) - Server: Express 5 + TypeScript
- Operator console: Vite + React + TypeScript (built to
public/console/, served static) - Control-plane lib: audit / policy / reliability / approvals / reservations / payroll / invoices / keeper / departments / reporting — pure, audit-derived, unit-tested (
node --test) - Runtime: tsx (Node.js)