Skip to content
Docs Architecture

Architecture

System design, middleware stack, request lifecycle, smart contracts, caching, data flow, and scaling characteristics. The single source of truth for "how does the service fit together" — the algorithm details live in concepts.md.

1. System overview

Agent Passport is a stateless HTTP API that scores Algorand wallets for trust, delegation trust, sybil risk, reputation, and creditworthiness, and exposes two on-chain mutating endpoints (/delegate, /revoke) backed by a TEAL stateful contract.

┌──────────────┐     ┌────────────────────────────────────────┐     ┌─────────────────────┐
│              │     │  Express on Node 20+ (port 3000)      │     │                     │
│  Client /    │────▶│   - Helmet, CORS, requestId           │────▶│  Algorand           │
│  Agent       │     │   - Rate limit (600/min/IP)           │     │  (algod + indexer)  │
│              │     │   - Metrics, x402, idempotency        │     │                     │
│  SDK (TS)    │     │   - LRU response cache (60s TTL)      │     │  + optional         │
│  SDK (Py)    │     │   - In-memory idempotency store (24h) │     │    registry.teal    │
│              │     │   - HMAC auth (state-changing)        │     │    reputation.teal  │
└──────────────┘     └────────────────────────────────────────┘     └─────────────────────┘

Stateless for read endpoints — every request fetches data from Algorand and caches in-memory for 60 s. No database, no Redis, no message queue.

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 cache
  • src/lib/security.ts — per-IP rate-limit map
  • src/lib/system-exposure.ts — global + per-wallet exposure ledger
  • src/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.

2. Request lifecycle

A request flows through twelve ordered middlewares plus the route handler. Each middleware is registered in src/app.ts.

#MiddlewarePurposeHeaders added
1app.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
2helmet()HSTS, X-Content-Type-Options, X-Frame-Options, CSPStrict-Transport-Security, etc.
3requestIdMiddlewareUUID per request; reads X-Request-ID if validX-Request-ID
4requestLoggingMiddlewareOne JSON log line per request
5corsMiddleware({ origin })CORS with * or allow-listAccess-Control-*
6rateLimiter({ windowMs, max })600 req/min/IP; bypasses for ops + trusted IPsX-RateLimit-*
7express.json({ limit: '100kb' })JSON body parser, hard 100 KB cap
8metricsMiddlewareRecords http_request_duration_seconds
9requestDeadlineMiddlewareSets res.locals.deadlineAt from REQUEST_TIMEOUT_MS
10x402MiddlewareWhen enabled, requires x-payment on premium endpointsx402 spec
11settlementVerificationMiddlewareAsync verify of payment on-chain
12hmacAuth (if HMAC_SECRET set)HMAC-SHA256 auth on mutating endpoints
13idempotencyMiddlewareIdempotency-Key handling for mutating callsidempotency-key, idempotent-replay
14Route handlerPer-endpoint logicper-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.tsTTLCache<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

microALGO units plus the last-update timestamp

  • App ID: REGISTRY_APP_ID env var
  • Global state: admin (operator address)
  • Box storage: one box per (sponsor, agent) pair, value = amount in
  • Methods:
  • add_delegation(sponsor, agent, amount) — creates a box
  • revoke_delegation(sponsor, agent) — deletes the box
  • update_admin(new_admin) — rotates the operator key
  • Update permission: only the admin address

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_ID env var
  • Global state: admin, event_count, per-wallet event counter
  • Local state: per-account event log
  • Methods:
  • record_event(wallet, event_type, amount, counterparty) — appends
  • update_admin(new_admin) — rotates the operator key
  • Update permission: only the admin address

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)

client            service                 algod     indexer      contract
  │                  │                       │           │            │
  │─GET /score──▶    │                       │           │            │
  │                  │─status()───────────▶ │           │            │
  │                  │◀─────lastRound───────│           │            │
  │                  │─accountInformation(w)─▶│           │            │
  │                  │◀─────info─────────────│           │            │
  │                  │─────────────────────  │           │            │
  │                  │ (5 sub-scores, parallel calls)    │            │
  │                  │                       │           │            │
  │◀────200 JSON─────│                       │           │            │

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

FilePurposePublic exports
src/trust-score.tsComposite trust scorescoreWallet, scoreWalletFresh
src/delegation.tsSponsor graph BFSscoreDelegation, scoreDelegationFresh
src/sybil.ts12 sybil signalsdetectSybil, detectSybilFresh
src/reputation.tsEvent log + scorecomputeReputation, recordEvent
src/credit.tsCapacity estimateestimateCredit, estimateCreditWithTrust
src/underwriting.tsDecision engineunderwrite
src/passport.tsFull documentgeneratePassport
src/trust-graph.tsGraph analyticsanalyzeTrustGraph, simulateSponsorLoss
src/counterparty.tsBuyer risk checkcheckCounterparty
src/registry.tsOn-chain delegatedelegate, revoke
src/lib/cache.tsLRU TTL cacheTTLCache
src/lib/idempotency.tsIdempotency-KeyidempotencyMiddleware
src/lib/security.tsCORS, rate limit, HMACcorsMiddleware, rateLimiter, hmacAuth
src/lib/metrics.tsPrometheusmetricsMiddleware
src/lib/x402.tsx402 paymentx402Middleware
src/lib/hmac-auth.tsHMAC authhmacAuth
src/lib/operator-wallet.tsOperator wallet initinitOperatorWallet
src/lib/system-exposure.ts$100k capaddSystemExposure, capToSystemCapacity
src/lib/webhooks.tsSubscriber registryaddSubscriber, fireWebhook
src/lib/sanctions.tsDeny-list providergetSanctionsProvider, checkSanctions
src/lib/timeout.tswithTimeoutwithTimeout
src/lib/json-store.tsJSON-file persistencequeueJsonWrite, readJsonFile
src/lib/request-deadline.tsPer-request deadlinerequestDeadlineMiddleware
src/lib/graph.tsSybil graph signals(private)
src/lib/build-info.tsVersion metadatabuildInfo, packageVersion

7. Scaling characteristics

ResourceBoundNotes
MemoryO(active wallets in 60s window × cache entry size)TTLCache caps at 500 entries
Algorand RPC1 round-trip per sub-score + 1 for statusIndexed per wallet; cache mitigates
Multi-replicaEach replica is independentIdempotency + rate-limit + exposure need Redis for cross-replica consistency
Cold start~1s to import + 0 external callsNo DB to warm
Graceful shutdown10s forced exit after SIGTERMmetricsCollectors, idempotencySweeper, rateLimitTimer all stop

For horizontal scaling:

TRUST_PROXY_HOPS to the number of trusted hops. The default of 0 is fail-closed (does not honour the spoofable header); set to 1 for a single reverse proxy (ALB, nginx), to 2 for CloudFront → ALB → app, and to 2 or 3 for k8s ingress → sidecar → app depending on the topology.

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. The src/lib/json-store.ts interface is designed to be drop-in replaceable.

livenessProbe to /health.

  1. Add a load balancer that forwards X-Forwarded-For and set
  2. Set REPLICA_COUNT to the planned replica fan-out. The service
  3. Configure your orchestrator's readinessProbe to /ready and