Architecture
This guide explains how Agent Passport turns a wallet request into an explainable decision: the service boundary, request lifecycle, Algorand data flow, contracts, caching, persistence, and scaling model. For the scoring rules themselves, see the algorithm and scoring reference.
1. System overview
Agent Passport is a self-hosted HTTP API for evaluating Algorand agents. Read endpoints return trust, delegation, Sybil, reputation, credit, and underwriting evidence. Mutating delegation and reputation routes can write to the configured Algorand applications when an operator wallet is enabled.
Client / agent
HTTP, TypeScript, or Python SDK
Agent Passport API
Validates, authenticates, scores, and explains.
Algorand
algod, indexer, and optional applications.
Read endpoints do not require an application database: they fetch from the configured Algorand algod and indexer services and use a 60-second in-memory response cache. The service does not silently provision hosted storage, queues, billing, or identity infrastructure.
For stateful endpoints, four per-process stores are NOT shared across pods and must be backed by Redis (or equivalent) when running with REPLICA_COUNT > 1:
src/lib/idempotency.ts— Idempotency-Key cachesrc/lib/security.ts— per-IP rate-limit mapsrc/lib/system-exposure.ts— global + per-wallet exposure ledgersrc/lib/webhooks.ts— webhook subscriber registry
A boot-time warning is logged for each store when REPLICA_COUNT > 1. The on-disk JSON files (data/*.json) only protect against restart loss, not against pod fan-out — each replica independently enforces its cap and serves its subscriber list. Scale reads by adding pods; scale state by adding Redis.
HTTP parsing stays at the application edge. Shared wallet and amount rules live in src/http/validation.ts, while domain modules remain independent of Express request and response objects. New routes should validate and normalize at this boundary, call a domain function, and then map its result to HTTP.
2. Request lifecycle
A request passes through ordered platform and security stages before its route handler. The stack validates input, assigns a request ID, applies CORS and rate limits, enforces deadlines and optional payment/authentication, and then dispatches to the capability. Operational routes are kept separate from business logic so health, readiness, metrics, and version metadata remain safe to expose to deployment tooling.
| # | Middleware | Purpose | Headers added |
|---|---|---|---|
| 1 | app.set('trust proxy', TRUST_PROXY_HOPS) | Honour X-Forwarded-For from N trusted proxy hops; 0 (default) trusts nothing, 1 trusts one hop, 2+ trusts N hops for CloudFront → ALB → app style chains | — |
| 2 | helmet() | HSTS, X-Content-Type-Options, X-Frame-Options, CSP | Strict-Transport-Security, etc. |
| 3 | requestIdMiddleware | UUID per request; reads X-Request-ID if valid | X-Request-ID |
| 4 | requestLoggingMiddleware | One JSON log line per request | — |
| 5 | corsMiddleware({ origin }) | CORS with * or allow-list | Access-Control-* |
| 6 | rateLimiter({ windowMs, max }) | 600 req/min/IP; bypasses for ops + trusted IPs | X-RateLimit-* |
| 7 | express.json({ limit: '100kb' }) | JSON body parser, hard 100 KB cap | — |
| 8 | metricsMiddleware | Records http_request_duration_seconds | — |
| 9 | requestDeadlineMiddleware | Sets res.locals.deadlineAt from REQUEST_TIMEOUT_MS | — |
| 10 | x402Middleware | When enabled, requires x-payment on premium endpoints | x402 spec |
| 11 | settlementVerificationMiddleware | Async verify of payment on-chain | — |
| 12 | hmacAuth (if HMAC_SECRET set) | HMAC-SHA256 auth on mutating endpoints | — |
| 13 | idempotencyMiddleware | Idempotency-Key handling for mutating calls | idempotency-key, idempotent-replay |
| 14 | Route handler | Per-endpoint logic | per-endpoint |
Operational endpoints (/health, /ready, /health/deep, /metrics, /registry/status) are exempt from rate limiting (step 6) and from payment (step 10).
3. Cache and idempotency stores
Response cache
src/lib/cache.ts — TTLCache<unknown>(maxEntries: 500, ttlMs: 60_000). Caches /score and /passport for 60 s. Invalidated on /delegate, /revoke, /reputation/record for the affected wallet.
Idempotency store
src/lib/idempotency.ts — in-memory Map<key, { bodyHash, status, body, expiresAt }>. Default 24 h TTL, 10 000 entry cap, 5-minute sweeper. Body hash uses canonical JSON (sorted keys) so {"a":1,"b":2} and {"b":2,"a":1} produce the same hash.
For multi-replica deployments, back this with Redis. The current implementation is per-process.
Rate-limit and system-exposure JSON files
Two single-purpose data/*.json files persist across restarts:
data/rate-limit.json— per-IP request counts (resets after 60 s)data/system-exposure.json— cumulative approved credit per wallet
Both use the shared JSON-file persistence helper (src/lib/json-store.ts) with a write-queue mutex to prevent concurrent-write races. Multi-replica needs Redis for both.
4. Smart contracts
Two TEAL v10 contracts under contracts/:
registry.teal — delegation registry
- App ID:
REGISTRY_APP_IDenv var - Global state:
admin(operator address) - Box storage: one box per
(sponsor, agent)pair, value = amount in microALGO units plus the last-update timestamp - Methods:
add_delegation(sponsor, agent, amount)— creates a boxrevoke_delegation(sponsor, agent)— deletes the boxupdate_admin(new_admin)— rotates the operator key- Update permission: only the
adminaddress
The current transaction format passes the agent and sponsor as application accounts (Accounts[0] and Accounts[1]) and includes the deterministic box reference in the application call. Existing deployments created with the older payment-transfer format must be redeployed (or migrated with a planned contract upgrade) before enabling the updated service.
reputation.teal — on-chain reputation events
- App ID:
REPUTATION_APP_IDenv var - Global state:
admin,event_count, per-wallet event counter - Local state: per-account event log
- Methods:
record_event(wallet, event_type, amount, counterparty)— appendsupdate_admin(new_admin)— rotates the operator key- Update permission: only the
adminaddress
Deploying the contracts: npm run deploy-registry and npm run deploy-reputation (uses DEPLOYER_MNEMONIC). The runtime operator wallet (OPERATOR_MNEMONIC) is a separate key with on-chain permission.
5. Data flow (request → Algorand → response)
- 01Request
GET /score?wallet=… - 02Gateway
Validate, identify, rate-limit
- 03Chain evidence
Round, account, transactions
- 04Decision
Five signals computed in process
- 05Response
Score, risk, evidence, checksum
Trust-score generation fans out 5 sub-score calls (age, activity, volume, velocity, compliance), each hitting algod once. The composite is computed in-process. Total: 1 status() + 1 accountInformation() + 0-25 txlist/txinfo for delegation, all in parallel where independent.
6. Module reference
| File | Purpose | Public exports |
|---|---|---|
src/trust-score.ts | Composite trust score | scoreWallet, scoreWalletFresh |
src/delegation.ts | Sponsor graph BFS | scoreDelegation, scoreDelegationFresh |
src/sybil.ts | 12 sybil signals | detectSybil, detectSybilFresh |
src/reputation.ts | Event log + score | computeReputation, recordEvent |
src/credit.ts | Capacity estimate | estimateCredit, estimateCreditWithTrust |
src/underwriting.ts | Decision engine | underwrite |
src/passport.ts | Full document | generatePassport |
src/trust-graph.ts | Graph analytics | analyzeTrustGraph, simulateSponsorLoss |
src/counterparty.ts | Buyer risk check | checkCounterparty |
src/registry.ts | On-chain delegate | delegate, revoke |
src/lib/cache.ts | LRU TTL cache | TTLCache |
src/lib/idempotency.ts | Idempotency-Key | idempotencyMiddleware |
src/lib/security.ts | CORS, rate limit, HMAC | corsMiddleware, rateLimiter, hmacAuth |
src/lib/metrics.ts | Prometheus | metricsMiddleware |
src/lib/x402.ts | x402 payment | x402Middleware |
src/lib/hmac-auth.ts | HMAC auth | hmacAuth |
src/lib/operator-wallet.ts | Operator wallet init | initOperatorWallet |
src/lib/system-exposure.ts | $100k cap | addSystemExposure, capToSystemCapacity |
src/lib/webhooks.ts | Subscriber registry | addSubscriber, fireWebhook |
src/lib/sanctions.ts | Deny-list provider | getSanctionsProvider, checkSanctions |
src/lib/timeout.ts | withTimeout | withTimeout |
src/lib/json-store.ts | JSON-file persistence | queueJsonWrite, readJsonFile |
src/lib/request-deadline.ts | Per-request deadline | requestDeadlineMiddleware |
src/lib/graph.ts | Sybil graph signals | (private) |
src/lib/build-info.ts | Version metadata | buildInfo, packageVersion |
7. Scaling characteristics
| Resource | Bound | Notes |
|---|---|---|
| Memory | O(active wallets in 60s window × cache entry size) | TTLCache caps at 500 entries |
| Algorand RPC | 1 round-trip per sub-score + 1 for status | Indexed per wallet; cache mitigates |
| Multi-replica | Each replica is independent | Idempotency + rate-limit + exposure need Redis for cross-replica consistency |
| Cold start | ~1s to import + 0 external calls | No DB to warm |
| Graceful shutdown | 10s forced exit after SIGTERM | metricsCollectors, idempotencySweeper, rateLimitTimer all stop |
For horizontal scaling:
- Add a load balancer that forwards
X-Forwarded-Forand setTRUST_PROXY_HOPSto the number of trusted hops. The default of0is fail-closed (does not honour the spoofable header); set to1for a single reverse proxy (ALB, nginx), to2for CloudFront → ALB → app, and to2or3for k8s ingress → sidecar → app depending on the topology. - Set
REPLICA_COUNTto the planned replica fan-out. The service will emit a boot-time warning for each per-process store (rate-limit, idempotency, system-exposure, webhook subscribers) so you remember to back them with Redis. Thesrc/lib/json-store.tsinterface is designed to be drop-in replaceable. - Configure your orchestrator's
readinessProbeto/readyandlivenessProbeto/health.