Policy-aware spend monitoring & multi-account treasury management for x402 agentic payments on Stellar
Watch how autonomous agents spend. Know before limits are broken. Never touch a key.
SpendGuard is a read-only monitoring layer for the next generation of payments — the ones made by agents, not humans. It watches settled on-chain Stellar transfers against the spending-limit policies agents have declared on-chain (via OpenZeppelin's stellar-accounts smart account framework) and pairs that with an enterprise-grade multi-account treasury management system: RBAC, budgets, audit logs, and multi-sig approvals.
It never sits in the payment path — it observes and reports, independent of whichever client the agent used to send the transaction.
Companion contract repo:
spendguard-contract— the read-only Sorobanpolicy-view-helperthis indexer queries for live on-chain policy state.
| SpendGuard is | SpendGuard never |
|---|---|
| ✅ A passive observer of on-chain spend | ❌ Custodies, holds, or touches keys |
| ✅ A continuous, cursor-persisted event indexer | ❌ Signs or submits transactions |
| ✅ A live reader of on-chain spending policies | ❌ Blocks, freezes, or enforces anything |
| ✅ A multi-account treasury dashboard with RBAC | ❌ Sits in the payment path |
If SpendGuard is late or wrong, no funds are at risk as a direct result — the real backstop is the on-chain policy enforcement it observes.
x402 enables autonomous agents to execute payments on Stellar without human approval for every micro-transaction. Smart-contract policies enforce limits on-chain — but operators and treasury managers lack an independent, consolidated view of whether those limits are being approached or broken across multiple accounts.
SpendGuard closes that gap with a passive, non-custodial observability and governance layer:
- 🔁 Continuous event indexing — ingests and persists real on-chain SEP-41
transferand OpenZeppelinspending_limit_enforcedevents in Postgres, keeping historical visibility beyond Soroban RPC's ~7-day retention window. - 📖 Live policy reads — cross-references each transfer against the spending account's own declared policy, read live from the deployed
policy-view-helpercontract (no stale cached state). - 🚨 Breach & near-miss alerts — flags breaches and near-misses (default: 90% of cap) via webhook and dashboard.
- 🏦 Multi-account treasuries — unlimited accounts (
Personal,Business,DAO,NGO,Project) with fully isolated data domains. - 🛡️ Strict 5-tier RBAC —
Owner,Admin,Finance Manager,Approver,Viewerenforced across APIs and UI. - ✍️ Multi-sig approvals, budgets, audit trails — built-in governance for high-value treasury disbursements.
Monitoring core
- Continuous Soroban RPC polling with Postgres-persisted ledger cursor (no history lost to RPC retention)
- Live on-chain policy reads via
simulateContract/simulateTransaction - Breach and near-miss detection (configurable threshold, default 90% of cap)
- Webhook alert delivery + per-account alert timeline in the dashboard
Treasury dashboard (Next.js)
- Multi-account treasury management with typed accounts and isolated data domains
- Spending policies, budgets (Monthly/Quarterly/Annual), and multi-sig approval workflows with voter progress
- Members & RBAC with permission checks on every API route and UI action
- Immutable security & operations audit log and per-account settings (webhooks, thresholds, auto-lock)
- Real-time spend timeline, KPI cards, and alert timeline
- 🌗 Polished light/dark theme
▶️ Runs out of the box with seeded demo data when no database is configured
sequenceDiagram
autonumber
participant A as Agent (x402 client)
participant SA as OZ Smart Account
participant RPC as Soroban RPC
participant IX as SpendGuard Indexer
participant DB as Postgres
participant WB as Dashboard / Webhooks
A->>SA: Send transfer (spending_limit enforced on-chain)
SA-->>RPC: Emits transfer event
loop Every poll interval
IX->>RPC: getEvents(from persisted cursor)
RPC-->>IX: New transfer events
IX->>RPC: simulateContract → live spending_limit state
IX->>DB: Persist events + advance cursor
IX->>IX: Compare vs cap → breach / near-miss (≥ 90%)
alt Alert raised
IX->>WB: Webhook POST push
IX->>DB: Persist alert
end
end
WB->>DB: Accounts, spend history & alert timeline
spendguard-app/
├── packages/sdk/ # Soroban RPC client, live policy reads, XDR helpers, RBAC engine
├── indexer/ # Long-running event-ingest service + breach detector + webhook dispatcher
├── apps/web/ # Next.js 15 dashboard — real-time, never signs on-chain
└── .github/ # CI (install, typecheck, lint, test) + Drips wave templates
packages/sdk— typed wrapper around Stellar Soroban:soroban-client(getEvents,getLatestLedger,simulateContract),policy-reader(livespending_limitreads viasimulateTransaction),xdr-helpers(strongly-typednativeToScVal/scValToNative), plus a pure-RBACpermissionsengine.indexer— a persistent process, not a serverless function: Soroban RPC's ~7-day event retention makes continuous polling a hard requirement.event-polleradvances a Postgres-persisted cursor;breach-detectorcompares each event against the account's live cap;alert-dispatcherdelivers webhooks.apps/web— Next.js 15 + React 19 + Tailwind dashboard. API routes read the same Postgres database (with an in-memory fallback so the UI is demoable anywhere).
| Permission | Owner | Admin | Finance Manager | Approver | Viewer |
|---|---|---|---|---|---|
account:create |
✅ | ❌ | ❌ | ❌ | ❌ |
account:edit |
✅ | ✅ | ❌ | ❌ | ❌ |
account:archive / account:delete |
✅ | ❌ | ❌ | ❌ | ❌ |
members:manage |
✅ | ✅ | ❌ | ❌ | ❌ |
policies:manage / budgets:manage |
✅ | ✅ | ✅ | ❌ | ❌ |
multisig:create |
✅ | ✅ | ✅ | ❌ | ❌ |
multisig:approve |
✅ | ✅ | ❌ | ✅ | ❌ |
data:view |
✅ | ✅ | ✅ | ✅ | ✅ |
SpendGuard is built on a simple principle: the least powerful tool that solves the problem.
- 🔑 No private keys, no signing, no transaction submission — anywhere in this repo
- 👀 Reads only public on-chain data
- 📦 Attack surface limited to event-decoding correctness, alert delivery, and API access control
- 📖 See SECURITY.md for the full policy and responsible-disclosure process
git clone https://github.com/Spendguard/spendguard-app.git
cd spendguard-app
npm install
npm run buildCreate a .env.local (root or apps/web/.env.local). The values below are the live testnet configuration this repo is developed against:
DATABASE_URL="postgres://postgres:postgres@localhost:5432/spendguard"
SOROBAN_RPC_URL="https://soroban-testnet.stellar.org"
NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
POLICY_VIEW_HELPER_CONTRACT_ID="CCAM4NRAUB6SO3XLL2SRZQEHSOUYQGDKGNPCUIQ5KKI2S6QKWC2VN6NX"
# SpendGuard-owned testnet USDC SEP-41 contract (issuer: X402_ASSET_ISSUER).
# Swap back to Circle's testnet USDC SAC (CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA)
# if you fund the monitored account from faucet.circle.com instead.
X402_ASSET_CONTRACT_ID="CD76G5V4M5BBLO2NRMKVCLLODPX5IIL6MZVRHUKB4NM3555EJMJFCXM6"
X402_ASSET_ISSUER="GBXCSJZADCXIWLZMSR4NGWR7BAS6JWBIBPWQ7VEBLZ7YTD7MW2G5OXV5"
X402_ASSET_ISSUER_SECRET="<issuer secret — only needed to mint>"
# Funded testnet account (Friendbot) — the monitored settlement source
SIMULATION_SOURCE_ACCOUNT="GAUNWPSA4UERYKTDIGAHPTGILYJ3YNSLMNU5EQYZFV65VAVMH6QXFNU5"
SIMULATION_SOURCE_ACCOUNT_SECRET="<account secret — only needed by send:transfer / trustline:usdc>"
# Optional: comma-separated OpenZeppelin smart-account contract IDs
# X402_SMART_ACCOUNT_CONTRACT_IDS=
NEAR_MISS_THRESHOLD_PCT=90
POLL_INTERVAL_MS=10000npm run typecheck # tsc --noEmit across all workspaces
npm test # Vitest suites across SDK, Indexer, and Web (64 tests)
# Terminal A — indexer (watch mode)
npm -w @spendguard/indexer run dev
# Terminal B — dashboard
npm -w @spendguard/web run devOpen http://localhost:3000.
No database handy? The dashboard degrades gracefully — every screen renders with seeded demo data, so you can explore the full UI before wiring up Postgres.
Requirements: Node ≥ 18.18 (CI runs Node 22 LTS) · PostgreSQL 15+ · a deployed policy-view-helper contract (testnet or mainnet).
The whole pipeline runs against soroban-testnet.stellar.org — this is the exact flow used to produce the live data in this repo:
# 1. (once) Let the funded account hold the monitored asset
npm -w @spendguard/indexer run trustline:usdc # adds a USDC trustline to SIMULATION_SOURCE_ACCOUNT
# 2. Tell the indexer whose transfers count as settlements
npm -w @spendguard/indexer run monitor:account # registers SIMULATION_SOURCE_ACCOUNT with context rule 1
# 3. Generate real on-chain activity (optional — the indexer ingests any SEP-41 transfer)
npm -w @spendguard/indexer run send:transfer -- 5 # sends 5 USDC to a fresh Friendbot-funded destination
# 4. Run the indexer — polls Soroban RPC, persists events + ledger cursor to Postgres
npm -w @spendguard/indexer run dev
# 5. Query the ingested events (newest first)
curl "http://localhost:3000/api/accounts?address=G...&events=true"The dashboard's Spend History chart and Transactions tab read these ingested events directly (30 s auto-refresh) — send a transfer and it appears within a poll cycle.
| Command | Description |
|---|---|
npm run build |
Builds @spendguard/sdk, @spendguard/indexer, @spendguard/web in order |
npm run typecheck |
Runs tsc --noEmit across all workspace projects |
npm run lint |
Runs ESLint across TypeScript and React codebases |
npm test |
Runs Vitest suites across SDK, Indexer, and Web app workspaces |
npm -w @spendguard/web run dev |
Launches the Next.js dev server on port 3000 |
npm -w @spendguard/indexer run dev |
Runs the indexer continuous polling loop with tsx |
npm -w @spendguard/indexer run monitor:account |
Registers an address as a monitored account (-- G... [context_rule_id] [label]; defaults to SIMULATION_SOURCE_ACCOUNT, rule 1) so its transfers count as settlements |
npm -w @spendguard/indexer run trustline:usdc |
Adds the monitored-asset trustline (-- G...; defaults to SIMULATION_SOURCE_ACCOUNT) — required before an account can send or receive the asset |
npm -w @spendguard/indexer run send:transfer |
Sends a real testnet transfer (-- <amount_USDC> [destination]; default 5 USDC, funds a fresh Friendbot destination) to generate live on-chain activity |
CI (GitHub Actions) runs install, typecheck, lint, and tests on every push and pull request to main.
GET/POST /api/accounts— list (status/type/search filters) / create treasury accountsGET /api/accounts?address=G…&events=true— live ingested settlement events (SEP-41transfer/spending_limit_enforced) for an address, newest first — the data behind the dashboard's Spend History chart and Transactions tabGET /api/accounts?address=G…&alerts=true— breach/near-miss alerts for an accountGET /api/stellar/account?address=G…&ruleId=…— live on-chain account state (sequence, XLM balance via Horizon REST, RPC reachability)GET/PATCH /api/accounts/[id]— account detail & updateGET/POST /api/accounts/[id]/members·/policies·/budgets·/multisig·/settings·/audit-logsPOST /api/webhooks— alert delivery endpoint
- Live testnet flow verified, but not yet a CI golden-fixture test. Real SEP-41 transfers have been sent, ingested, and rendered end-to-end (transfer → indexer → Postgres → dashboard/API) against this repo's live testnet configuration. That verification is currently manual — the automated suite still relies on encode/decode round-trips plus one captured real on-chain value.
- Only
spending_limit-policy accounts are supported so far (simple_threshold,weighted_thresholdare next). spending_limitis transfer-context only — an upstream OpenZeppelin constraint, not a SpendGuard one.
SpendGuard is intentionally scoped to prove policy-aware, real on-chain observability first — not premature production hardening.
- 🧪 Turn the verified live testnet flow into an automated golden-fixture test in CI
- 🧩 Support additional OZ policy types (
simple_threshold,weighted_threshold) - 🔔 PagerDuty / Slack alert sinks alongside webhooks
- 🧑💻 Real auth (OAuth / wallet sign-in) replacing the role switcher demo
Contributors are welcome — and rewarded. SpendGuard is a great Drips-wave contribution target: well-scoped issues, a tiny codebase, and zero blockchain setup required to run the UI (seeded demo data).
- 🎯 Start with issues labelled good first issue
- 📖 Read CONTRIBUTING.md — Conventional Commits, TypeScript strict, Vitest
- 🐛 Security-sensitive bugs go through SECURITY.md
- 🧪
npm test --workspaces --if-presentruns the Vitest suites
MIT © SpendGuard Contributors