Guardian watches Stellar accounts and Soroban contract activity in real time, scores each transaction for fraud risk, and enforces graduated protective actions — from alerting, to requiring multi-sig approval, to freezing assets — using Stellar's native account/asset controls plus a custom Soroban policy contract. Every enforcement decision is recorded as an on-chain event, so the audit trail is verifiable independently of this software.
Network: Stellar Testnet only. Mainnet is out of scope until explicitly enabled and the pre-Mainnet checklist is satisfied.
- Live contract:
CCCTWXPVGTN7KYTEG3XI2FQ33QTY7K2JUGTWBNIGNBJFGDDV3ANU6LOI(Testnet) - Tests: 27 contract (Rust) + 76 backend (TypeScript), all passing
- Docs: Threat model · Security review · Testnet deployment
- A false positive that freezes legitimate funds is the worst-case failure. Enforcement defaults to manual human approval; automation is opt-in and gated behind a conservative confidence bar.
- No silent enforcement. Every state-changing action (freeze, unfreeze, policy change) emits a typed Soroban event — the on-chain audit trail is a core feature, not a log.
- Native controls first. Stellar multi-sig and
AUTH_REQUIRED/AUTH_REVOCABLEasset flags are preferred over custom custody logic. The contract never holds funds. - Fail safe on enforcement, fail open on detection. No component failure can produce an erroneous freeze or silently unfreeze; a listener outage only delays detection, and the cursor replays missed transactions on recovery. See failure modes.
- Decoupled layers. The rules engine and enforcement bridge evolve without contract redeploys.
Stellar Testnet ledger
│ payments
▼
┌─────────────────────────────────────────────────────────┐
│ Horizon Listener ──▶ Postgres ◀── Guardian Events │
│ (cursor-resumable) (history, Listener │
│ policy mirror, (mirrors on-chain │
│ audit log) status + events) │
└───────────────┬─────────────────────────────────────────┘
│ onTransaction
▼
Risk Scoring Engine ──▶ score + tier (low / medium / high)
(5 pluggable rules)
│
▼
Enforcement Bridge ──┬─ low → audit only
(automation gate) ├─ medium → pending flag + alert
└─ high → manual: pending flag + alert
notify (≥95): auto-freeze + alert
│ approve │ flag_and_freeze
▼ ▼
Dashboard (Next.js) ◀── REST API ──▶ Guardian Contract (Soroban)
review queue, policy, set_policy / check_transaction /
audit log, Freighter flag_and_freeze / unfreeze / events
End-to-end flow: a payment settles on-ledger → the listener ingests it and updates the
counterparty graph → the scoring engine assigns a risk score and tier → medium/high raises
a flag and an alert → a human approves in the dashboard (or, for notify accounts at very
high confidence, the gate auto-freezes) → the contract's flag_and_freeze runs and emits
events → a frozen account is released only by a multi-sig unfreeze from its configured
approvers.
contracts/guardian/ Soroban policy contract (Rust) — policy, freeze/unfreeze, events
backend/ TypeScript service:
src/listener/ Horizon + Soroban RPC listeners (cursor-resumable)
src/scoring/ pluggable rules engine + context builder
src/enforcement/ tier routing, automation gate, review service, contract client
src/api/ REST API (Express)
src/db/ Postgres schema, migrations, repositories
dashboard/ Next.js console — review queue, policy config, audit log, Freighter
docs/ threat-model.md, security-review.md, deployments/
Five heuristics each contribute a weighted score; the total is capped at 100 and mapped to a tier. The engine is pluggable — new rules are added without touching the pipeline, and a broken rule is skipped rather than crashing ingestion.
| Rule | Fires when | Score |
|---|---|---|
| Denylist | Counterparty is on the maintained denylist | 100 |
| Size anomaly | Amount ≥ 3× the account's historical p95 (needs ≥5 tx of history) | 25–40 |
| Velocity | > 10 transactions in a 5-minute rolling window | 25–40 |
| Structuring | ≥ 3 transactions within 10% below the policy limit in 1 hour | 40 |
| New counterparty | First-ever transaction with this counterparty | 15 |
Tiers: low < 40 → audit only · medium 40–69 → flag + alert · high ≥ 70 → flag +
alert (+ auto-freeze only if the account is notify and score ≥ 95).
- Rust (stable, 1.84+) with the
wasm32v1-nonetarget:rustup target add wasm32v1-none - stellar-cli:
cargo install --locked stellar-cli - Node.js ≥ 20
- PostgreSQL ≥ 14
# 1. Contracts — build and test
cd contracts
cargo test # 27 tests
cargo build --target wasm32v1-none --release # produces guardian.wasm
# 2. Database
createdb guardian # or: psql -c 'CREATE DATABASE guardian'
# 3. Backend
cd ../backend
npm install
cp .env.example .env # then edit (see Configuration below)
npm run migrate # apply schema
npm test # 76 tests (needs a reachable Postgres)
npm run dev # API on :4000 + listener/scoring loop
# 4. Dashboard
cd ../dashboard
npm install
echo "NEXT_PUBLIC_API_URL=http://localhost:4000" > .env.local
npm run dev # console on :3000The backend must have DASHBOARD_ORIGIN set to the dashboard's origin (e.g.
http://localhost:3000) or the browser's CORS preflight will block API calls.
Backend environment (backend/.env, see .env.example):
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
yes | Postgres connection string |
GUARDIAN_CONTRACT_ID |
yes | Deployed Guardian contract (C…) |
HORIZON_URL |
— | Horizon endpoint (default: Testnet) |
SOROBAN_RPC_URL |
— | Soroban RPC endpoint (default: Testnet) |
NETWORK_PASSPHRASE |
— | Network passphrase (Mainnet is refused at startup) |
ENFORCEMENT_SECRET_KEY |
for freezes | Admin key that signs flag_and_freeze; never commit it |
DASHBOARD_ORIGIN |
for the UI | Exact browser origin allowed via CORS |
ALERT_WEBHOOK_URL |
— | Webhook for high-risk alerts |
POLL_INTERVAL_MS |
— | Listener poll cadence (default 5000) |
PORT |
— | API port (default 4000) |
Dashboard: NEXT_PUBLIC_API_URL (backend base URL).
# One-time: create and fund a deployer/admin identity via Friendbot
stellar keys generate guardian-deployer --network testnet --fund
cd contracts
cargo build --target wasm32v1-none --release
stellar contract deploy \
--wasm target/wasm32v1-none/release/guardian.wasm \
--source guardian-deployer --network testnet \
-- --admin "$(stellar keys address guardian-deployer)"The --admin constructor argument sets the enforcement-service key (the only key that may
freeze). The printed contract ID goes into backend/.env as GUARDIAN_CONTRACT_ID; the
admin secret goes into ENFORCEMENT_SECRET_KEY. See
docs/deployments/testnet.md for the current deployment.
Base URL http://localhost:4000. All responses use the envelope
{ success, data, error }.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check |
GET |
/accounts |
List monitored accounts |
POST |
/accounts |
Start monitoring { address, label? } |
DELETE |
/accounts/:address |
Stop monitoring |
POST |
/accounts/:address/automation |
Set automation { level: "manual" | "notify" } |
POST |
/accounts/:address/request-review |
Fast-track appeal { reviewer, note? } |
GET |
/accounts/:address/transactions |
Recent transactions (?limit) |
GET |
/accounts/:address/policy |
Mirrored policy + live on-chain status |
GET |
/flags |
Review queue (?status=pending|approved|dismissed) |
POST |
/flags/:id/approve |
Approve → on-chain freeze { reviewer, note? } |
POST |
/flags/:id/dismiss |
Dismiss as false positive { reviewer, note? } |
POST |
/denylist |
Add counterparty { address, reason } |
DELETE |
/denylist/:address |
Remove from denylist |
GET |
/audit |
Audit trail, newest first (?limit) |
Approving a flag is the only path from the API to an on-chain freeze, and it is fully audited.
set_policy · check_transaction (read-only) · flag_and_freeze (admin only) ·
unfreeze (multi-sig) · get_policy / get_status / get_flag / get_admin. Events:
policy_set, account_flagged, account_frozen, account_unfrozen.
cd contracts && cargo test # 27 contract tests
cd backend && npm test # 76 tests (repos, listeners, scoring, enforcement, API)Backend tests run against a real Postgres (TEST_DATABASE_URL, default a guardian_test
database), created automatically on first run.
- Threat model — assets, trust boundaries, threats/mitigations, and explicit failure modes (fail-open detection, fail-safe enforcement).
- Security review — including GRD-1 (HIGH, fixed): a frozen account's owner could rewrite its own unfreeze policy and self-release; the contract now blocks non-admin policy edits while frozen.
Secrets are never committed: the enforcement key lives only in local stellar-cli identities
and .env (gitignored). A Mainnet passphrase is rejected at backend startup.
| Phase | Scope | Status |
|---|---|---|
| 0 | Monorepo scaffold, CI, Testnet accounts | ✅ |
| 1 | Guardian Soroban contract + tests + Testnet deploy | ✅ |
| 2 | Off-chain listener + Postgres storage | ✅ |
| 3 | Rule-based risk scoring engine | ✅ |
| 4 | Enforcement bridge (manual approval first) | ✅ |
| 5 | Dashboard | ✅ |
| 6 | Gated automation | ✅ |
| 7 | Hardening, threat model, Mainnet pilot plan | ✅ |