Skip to content
Docs Operations

Operations

How to run, deploy, monitor, and shut down the Agent Passport service. Source-of-truth for: environment variables, deployment, observability (metrics, SLOs, alerts, runbooks), rate limiting, idempotency, system-exposure cap, graceful shutdown, and load testing.

1. Environment variables

The canonical env-var table. Every variable the service reads is listed here, sourced from .env.example and src/config.ts. If you add a new env var, update this page, .env.example, and src/config.ts together.

Copy .env.example to .env and edit. The service calls dotenv.config() on startup so .env is loaded automatically.

Service

VariableTypeDefaultDescription
PORTint3000HTTP listen port
NODE_ENVenumdevelopmentRuntime mode; production requires HMAC auth and explicit CORS origins
LOG_LEVELenuminfodebug \info \warn \error
LOG_FORMATenumautojson or pretty
LOG_STDOUTbooltrueDisable stdout logging only for controlled file-log deployments
LOG_FILEpath—JSON log file (optional)
LOG_ERROR_FILEpath—Error-only log file (optional)
CORS_ALLOWED_ORIGINSstring*Comma-separated origins, or *
REQUEST_TIMEOUT_MSint10000Per-request Algorand/x402 timeout
REPLICA_COUNTint1Planned application replica count; values above 1 require shared state backends
TRUST_PROXY_HOPSint0Number of trusted reverse-proxy hops used for client IP resolution

Algorand

VariableTypeDefaultDescription
ALGOD_URLURLhttps://testnet-api.algonode.cloud:443algod v2 endpoint
ALGOD_TOKENstring""API token (AlgoNode free tier does not require one)
INDEXER_URLURLhttps://testnet-idx.algonode.cloud:443Indexer v2 endpoint
INDEXER_TOKENstring""API token
ALGO_NETWORKstringtestnetDisplay only

For mainnet, point at AlgoNode mainnet, a hosted provider (Nodely, BCC), or your own node. See § 4 for latency trade-offs.

Smart contracts

VariableTypeDefaultDescription
REGISTRY_APP_IDint0App ID of registry.teal. Set to 0 to disable /delegate and /revoke
REPUTATION_APP_IDint0App ID of reputation.teal. Set to 0 to disable /reputation/record on-chain writes
OPERATOR_MNEMONICstring—25-word Algorand mnemonic for the runtime operator wallet
DEPLOYER_MNEMONICstring—25-word mnemonic used only by the deploy scripts

See the operator-wallet security guide for the operator mnemonic handling and KMS guidance.

x402

VariableTypeDefaultDescription
X402_ENABLEDboolfalseWhen true, every premium endpoint requires an x-payment header
X402_FACILITATOR_URLURLhttps://x402.org/facilitatorx402 facilitator endpoint
X402_PAYMENT_RECIPIENTstring—Algorand address that receives USDC payments (required when x402 is enabled)
X402_NETWORKstringalgorand-testnetSupported x402 network identifier

Rate limiting

VariableTypeDefaultDescription
RATE_LIMIT_MAXint600Per-IP requests per minute
RATE_LIMIT_TRUSTED_IPSstring—Comma-separated IPs exempt from the limit
RATE_LIMIT_PERSISTENCE_PATHpathdata/rate-limit.jsonWhere to persist state across restarts
RATE_LIMIT_OVERRIDESJSON—Per-endpoint override, e.g. '{"POST /delegate":{"max":5}}'

Persistence

VariableTypeDefaultDescription
EXPOSURE_PERSISTENCE_PATHpathdata/system-exposure.jsonWhere to persist cumulative system exposure
WEBHOOKS_PERSISTENCE_PATHpathdata/webhooks.jsonWhere to persist webhook subscribers

Idempotency

The idempotency store is configured entirely in code (no env vars):

SettingValueSource
Idempotency-Key length8–255 chars, [A-Za-z0-9_\-:]+src/lib/idempotency.ts:5-7
Default TTL24 hourssrc/lib/idempotency.ts:8
Sweeper interval5 minutessrc/lib/idempotency.ts:9
Max store size10 000src/lib/idempotency.ts:10

Auth (HMAC)

VariableTypeDefaultDescription
HMAC_SECRETstring—If set (≥ 32 chars), state-changing endpoints require HMAC-SHA256 auth
HMAC_TIMESTAMP_SKEW_MSint60000Max clock skew between client and server

Sanctions

VariableTypeDefaultDescription
SANCTIONS_EXTRA_DENYstring—Comma-separated wallets to add to the default deny list

System exposure cap (hard-coded)

ConstantValueSource
MAX_SYSTEM_EXPOSURE100_000 USDCsrc/lib/system-exposure.ts:23
MAX_WALLET_SHARE10_000 USDC (10% of total)src/lib/system-exposure.ts:24

Constants (hard-coded, no env var)

ConstantValuePurpose
WALLET_REGEX/^[A-Z2-7]{58}$/Algorand address validator
MICRO_ALGO1_000_000Algo → microAlgo
SECONDS_PER_BLOCK3.3Average Algorand block time
TESTNET_GENESIS_ROUND64_600_000Testnet genesis round
MAX_ROUNDS_LOOKBACK1_000_000Cap for indexer queries

x402 pricing (X402_PRICING in src/lib/constants.ts)

EndpointPrice (USDC)
/score0.001
/delegation0.001
/counterparty-check0.002
/credit-estimate0.002
/sybil-check0.003
/reputation0.001
/reputation/record0.005
/underwrite0.01
/trust-graph0.005
/passport0.005

2. Rate limiting

Per-IP fixed-window rate limiter at src/lib/security.ts. Default 600 req/min/IP.

Configuration

Env varDefaultPurpose
RATE_LIMIT_MAX600Override the per-IP limit
RATE_LIMIT_TRUSTED_IPS—Comma-separated IPs exempt from the limit
RATE_LIMIT_PERSISTENCE_PATHdata/rate-limit.jsonWhere to persist state across restarts
RATE_LIMIT_OVERRIDES—Per-endpoint override as JSON

Bypass lists

The middleware short-circuits to next() in three cases:

  1. Operational endpoints. /health, /ready, /health/deep, /metrics, /registry/status are never rate-limited.
  2. LOAD_TEST_MODE=1. All rate limiting is disabled (k6 suite use; never in production).
  3. Trusted IPs. Any IP in RATE_LIMIT_TRUSTED_IPS bypasses the limit. Use for internal services, the operator host, or your monitoring agent.

Response headers

HeaderValue
X-RateLimit-LimitThe configured max (e.g. 600)
X-RateLimit-Remainingmax - count (clamped to 0)
X-RateLimit-ResetUnix-seconds when the current window expires

When the limit is exceeded, the response is 429 with body:

JSON example
{ "error": "Too many requests. Try again later." }

State is persisted to data/rate-limit.json via the shared json-store.ts write-queue mutex (race-free on concurrent saves).

3. Idempotency

The Idempotency-Key middleware (src/lib/idempotency.ts) makes mutating calls safe to retry. It applies only to non-GET / non- HEAD / non-OPTIONS requests; safe methods pass through with no key required.

Flow

For each request with a valid Idempotency-Key header:

  1. Look up the key in the in-memory store.
  2. On hit + same body hash → return the cached response with idempotent-replay: true.
  3. On hit + different body hash → return 409 Idempotency-Key reused with different request body and record a metric.
  4. On miss → require the client to send an Idempotency-Key (the middleware no longer auto-generates one). The request is processed and the response cached.

Header format

Text example
Idempotency-Key: <8-255 chars, [A-Za-z0-9_\-:]+>
  • 8–255 characters
  • Allowed: ASCII letters, digits, underscore, hyphen, colon
  • Anything else returns 400 Invalid Idempotency-Key format
  • Required on POST /delegate, POST /revoke, and POST /reputation/record — missing key returns 400

Body hashing

Body hash is sha256(canonicalJson(body)) where canonicalJson sorts keys recursively. So {"a":1,"b":2} and {"b":2,"a":1} produce the same digest. Same key + same body → cached response. Same key + different body → 409.

In-memory store

Map<string, IdempotencyRecord> at src/lib/idempotency.ts:21. Each record:

TypeScript example
interface IdempotencyRecord {
  key: string;
  bodyHash: string;     // sha256 hex
  status: number;       // HTTP status code (200-299)
  body: unknown;        // Response body
  createdAt: number;    // Unix ms
  expiresAt: number;    // Unix ms (createdAt + 24h)
}

The sweeper runs every 5 minutes:

  • Removes any record where expiresAt <= now
  • If the store is over MAX_STORE_SIZE = 10 000, evicts the oldest keys in insertion order (FIFO overflow)

The store is not persisted to disk. A process restart loses all in-flight idempotency state. Clients that retry with the same key after a server restart will re-execute the request.

Metrics

MetricTypeLabelsWhen
agent_passport_idempotency_hits_totalcounterpathReplay served from cache
agent_passport_idempotency_conflicts_totalcounterpathSame key, different body — 409

Endpoints requiring Idempotency-Key

EndpointRequired?Notes
POST /delegateYesOn-chain call — network fee on every retry
POST /revokeYesOn-chain call
POST /reputation/recordYesOn-chain call
POST /counterparty-checkNoIdempotent by nature (read-only)
POST /credit-estimateNoIdempotent by nature
GET /score, GET /passport, etc.N/AGETs are not idempotency-protected

Multi-replica

For deployments with > 1 replica, back the idempotency store with shared storage such as Redis. Without shared storage, two replicas can both receive the same key and execute the underlying operation; the middleware is not a globally coordinated exactly-once ledger.

4. Deployment

Quick start

Shell example
npm ci
cp .env.example .env
npm start

By default this points at the public Algorand testnet — no setup beyond the env file is needed.

Going to production — checklist

1. Choose your Algorand network

OptionWhen to useLatencySetup
Testnet (AlgoNode)Dev, staging, low-traffic production, MVP launches200-800ms per round-tripNone — defaults are set
Mainnet via public endpointProduction with relaxed SLOs (matches testnet numbers)200-800ms per round-tripUpdate ALGOD_URL and INDEXER_URL to mainnet
Mainnet via hosted provider (Nodely, BCC, AlgoNode paid tier)Production with stricter SLOs and zero node ops50-200ms per round-tripSubscribe to provider, set URLs
Mainnet via local Algorand nodeProduction needing the tightest SLOs (500ms P95)5-20ms per round-tripRun your own node — see Algorand node docs

The measured k6 testnet baseline (P95 < 1.5s, 99% availability) is what you should expect with any of the first three options. The local-node option is an upgrade path if you need the stricter prod-strict SLOs (P95 < 500ms, 99.9% availability). See § 7.

2. Set ALGOD_URL and INDEXER_URL

Shell example
# Testnet (default — no change needed)
ALGOD_URL=https://testnet-api.algonode.cloud:443
INDEXER_URL=https://testnet-idx.algonode.cloud:443

# Mainnet via AlgoNode
ALGOD_URL=https://mainnet-api.algonode.cloud:443
INDEXER_URL=https://mainnet-idx.algonode.cloud:443

3. Set the operator mnemonic

Shell example
OPERATOR_MNEMONIC="word1 word2 ... word25"

Or load from a secret manager at startup. See the operator-wallet security guide for KMS guidance.

Shell example
HMAC_SECRET="$(openssl rand -hex 32)"   # 64 hex chars = 256 bits

Any state-changing endpoint will then require HMAC-SHA256 authentication. Public reads and the operational endpoints remain unauthenticated in development. Production requires HMAC. See the HMAC authentication guide.

5. Build the Docker image

Shell example
docker build -t agent-passport:1.0.0 .
docker run --rm -p 3000:3000 --env-file .env agent-passport:1.0.0

The Dockerfile is multi-stage, runs as non-root, includes a liveness healthcheck at /health, and uses tini as PID 1 for proper signal forwarding. /ready remains the dependency-aware readiness probe and should be used for load balancer or orchestrator traffic gating. CI builds and starts this image with production settings as a release gate.

6. Kubernetes probe example

YAML example
livenessProbe:
  httpGet: { path: /health, port: 3000 }
  initialDelaySeconds: 10
  periodSeconds: 30
readinessProbe:
  httpGet: { path: /ready, port: 3000 }
  initialDelaySeconds: 5
  periodSeconds: 10

5. Observability

The service exposes Prometheus-format metrics at GET /metrics. This endpoint is:

  • Exempt from rate limiting (operational)
  • Always returns 200 unless the process is severely broken
  • Refreshes process gauges on every scrape (memory, CPU, uptime)

Metric inventory

API metrics

MetricTypeLabelsDescription
agent_passport_http_requests_totalcountermethod, path, status_classTotal HTTP requests
agent_passport_http_request_duration_secondshistogrammethod, path, status_classRequest duration in seconds
agent_passport_http_request_errors_totalcountermethod, path, status_class, error_type4xx/5xx errors

status_class is 2xx/3xx/4xx/5xx (not the raw status code) to bound label cardinality. Buckets for http_request_duration_seconds: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10.

Trust engine metrics

MetricTypeLabelsDescription
agent_passport_trust_score_generations_totalcounterrisk_levelTrust score computations, by risk bucket
agent_passport_trust_score_duration_secondshistogram—Trust score computation duration
agent_passport_graph_traversal_duration_secondshistogram—Graph traversal duration
agent_passport_graph_traversal_depthhistogram—Graph traversal depth (hops)
agent_passport_graph_traversal_depth_sumcounter—Cumulative graph traversal depth

x402 payment metrics

MetricTypeLabelsDescription
agent_passport_x402_payments_verified_totalcounterstatus, pathSuccessful verifications
agent_passport_x402_payment_failures_totalcounterreason, pathFailed verifications
agent_passport_x402_replay_attempts_totalcounterpathReplay attack attempts
agent_passport_x402_settlement_failures_totalcounterreasonSettlement failures
agent_passport_x402_verification_duration_secondshistogram—Verification latency

Contract metrics

MetricTypeLabelsDescription
agent_passport_contract_endorsements_totalcounternetworkOn-chain endorsements
agent_passport_contract_revocations_totalcounternetworkOn-chain revocations
agent_passport_contract_disputes_totalcounternetworkOn-chain disputes
agent_passport_contract_success_events_totalcounternetworkOn-chain success events
agent_passport_contract_event_stall_secondsgauge—Seconds since last contract event

Cache metrics

MetricTypeLabelsDescription
agent_passport_cache_hits_totalcountercache_nameCache hits
agent_passport_cache_misses_totalcountercache_nameCache misses
agent_passport_cache_evictions_totalcountercache_nameCache evictions
agent_passport_cache_sizegaugecache_nameCurrent cache size

Business metrics

MetricTypeLabelsDescription
agent_passport_passports_generated_totalcounter—Total passports generated
agent_passport_paid_requests_totalcounterpathx402-paid requests
agent_passport_unique_walletsgauge—Distinct wallets seen since process start
agent_passport_trust_checks_totalcountertypeTrust check operations
agent_passport_underwriting_decisions_totalcounteroutcome (approved, denied)Underwriting outcomes
agent_passport_counterparty_checks_totalcounteroutcome (allow, deny)Counterparty check outcomes
agent_passport_verify_checks_totalcounterflag, resultPer-flag /verify outcomes
agent_passport_discovery_searches_totalcounterquery_class, result_count/discovery/search calls
agent_passport_idempotency_hits_totalcounterpathReplays served from idempotency cache
agent_passport_idempotency_conflicts_totalcounterpathSame key, different body — 409

Infrastructure metrics

MetricTypeLabelsDescription
agent_passport_process_cpu_usage_seconds_totalgauge—Process CPU time in seconds
agent_passport_process_cpu_usage_ratiogauge—Process CPU as ratio of one core (0-1)
agent_passport_process_memory_usage_bytesgaugetypeProcess memory (rss, heapUsed, heapTotal, external, arrayBuffers)
agent_passport_process_uptime_secondsgauge—Process uptime in seconds
agent_passport_system_memory_bytesgaugetype (total, free, used)Host memory
agent_passport_system_load_averagegaugewindow (1m, 5m, 15m)Host load average

Label cardinality rules

To keep Prometheus healthy:

  • path is normalized to the route template — never use the raw req.path which can include wallet addresses
  • method is GET/POST/etc. (bounded)
  • status_class is 2xx/3xx/4xx/5xx (bounded)
  • wallet is never a label — use unique_wallets gauge
  • error_type is client_error/server_error (bounded)
  • outcome is approved/denied or allow/deny (bounded)
  • risk_level is low/medium/high/critical (bounded)
  • network is testnet/mainnet (bounded)
  • flag is funded/active/empty/lookup_failed (bounded)

Scrape configuration

YAML example
scrape_configs:
  - job_name: agent-passport
    metrics_path: /metrics
    scrape_interval: 30s
    scrape_timeout: 10s
    static_configs:
      - targets: ['agent-passport:3000']

6. SLOs

The SLO files are split by deployment target:

DeploymentSLO FileUse for
Testnet, public mainnet endpoint, or any deployment using a public Algorand endpointalerts/slo-prod-relaxed.ymlDefault — recommended for most deployments
Mainnet via local Algorand node or premium hosted provider with low latencyalerts/slo-prod-strict.ymlFor deployments needing 500ms P95

Prod-relaxed SLOs (default, measured)

Based on the k6 load test run against the public Algorand testnet (AlgoNode free tier):

SLOProd-relaxed targetMeasured baseline30d window
Availability99.0%99.55% under 1000 VUyes
Latency P95< 1.5s1.15s (100 VU), 2.27s (1000 VU)yes
Latency P99< 3.0s2.19s (100 VU), 4.13s (1000 VU)yes
Throughput> 100 rps1,829 rps sustained (500 VU)rolling 5m

The prod-relaxed SLOs are realistic for any deployment using a public Algorand endpoint because:

  • AlgoNode's free tier rate-limits at ~1,000 req/s per IP, which produces natural 429s
  • Round-trips to a remote indexer/algod add 200-800ms latency per call
  • /underwrite makes 4-5 Algorand round-trips, so its P95 is bounded by 5 × 800ms = 4s in the worst case

Prod-strict SLOs (aspirational)

SLOProd-strict targetNotes
Availability99.9% over 30dAchievable with low-latency Algorand endpoint
Latency P95< 500ms over 30dAchievable with a local node, premium hosted provider, or geographic co-location
Latency P99< 1.5s over 30d
Throughput> 1,500 rpsMeasured under cache-friendly load

How to hit the prod-strict targets: any combination of:

  • Local Algorand node (drops per-round-trip from 200-800ms to 5-20ms — the single biggest lever)
  • Premium hosted mainnet provider (Nodely, BCC, AlgoNode paid tier)
  • Geographic co-location with an Algorand relay

The prod-relaxed figures are measured load-test observations, not an SLA or a guarantee for a particular deployment. Switch to prod-strict only if you need 500ms P95 and are willing to operate and measure the required infrastructure.

Per-endpoint latency projections

EndpointTestnet P95 (measured)Prod-strict P95 (projected)Algorand calls
/score1.1s200ms2-3
/delegation1.5s300ms3-4
/passport (cached)2.4ms<5ms0
/passport (cold)1.5s400ms6-8
/underwrite2.2s500ms8-12
/trust-graph2.0s600ms10+
/credit-estimate1.5s350ms4-5
/counterparty-check1.1s300ms3-4
/reputation1.0s250ms2
/verify<10ms<10ms0 (cache hit)
/discovery/search<5ms<5ms0 (in-memory)

7. Alert-to-runbook map

AlertRunbook
AgentPassportAPIDownalerts/runbooks/agent-passport-api-down.md
AlgorandDependencyDownalerts/runbooks/agent-passport-api-down.md
X402PaymentVerificationFailingalerts/runbooks/x402-verification-failure.md
ContractIndexingFailurealerts/runbooks/contract-indexing-failure.md
ContractEventStallalerts/runbooks/contract-indexing-failure.md
HighErrorRatealerts/runbooks/elevated-error-rate.md
High5xxRatealerts/runbooks/elevated-error-rate.md
ElevatedErrorRatealerts/runbooks/elevated-error-rate.md
ElevatedLatencyP95alerts/runbooks/elevated-latency.md
ElevatedLatencyP99alerts/runbooks/elevated-latency.md
ReplayAttackSpikealerts/runbooks/replay-attack-spike.md
UnusualTrafficPatternalerts/runbooks/unusual-traffic.md
UnusualGraphGrowthalerts/runbooks/graph-growth.md

Runbooks live in alerts/runbooks/<name>.md.

8. Dashboard

The Grafana dashboard JSON is at alerts/grafana-dashboard.json. It includes:

  • API Request Rate (overall and per-endpoint)
  • API Latency (P50/P95/P99)
  • Error Rate
  • Trust Score Latency
  • Graph Traversal Latency
  • x402 Payments (verified/failures/replay)
  • Contract Events (endorsements/revocations/disputes/success)
  • Process Memory (heap/RSS)
  • Process Uptime
  • Passports Generated
  • Unique Wallets
  • Cache Performance (hits/misses/evictions)

9. System exposure cap

  • MAX_SYSTEM_EXPOSURE = 100_000 USDC (hard-coded)
  • Per-wallet cap: MAX_SYSTEM_EXPOSURE / 10 = 10_000 USDC
  • Persisted to data/system-exposure.json via json-store.ts write-queue mutex
  • capToSystemCapacity(wallet, limit) returns min(limit, globalRemaining, walletRemaining) — the final amount reserved

Multi-replica needs Redis (or accept the overshoot) — the JSON file is per-process.

10. Load testing

k6 suite under load-tests/. Run via:

Shell example
brew install k6
cd load-tests
LOAD_TEST_MODE=1 ./run-all.sh

Four scenarios: 100 VU, 500 VU, 1000 VU, and sustained 12 rps. The sustained scenario is a smoke test that always passes; the VU scenarios require a local Algorand node to hit P95 < 1.5s.

Thresholds:

  • Error rate: < 1%
  • P95 latency: < 1.5s (prod-relaxed), < 500ms (prod-strict)
  • Throughput: documented per scenario

11. Graceful shutdown

The service registers signal handlers in src/index.ts:38-46 and a matching cleanup hook in src/app.ts.

Signal handlers

SignalHandlerEffect
SIGTERMgracefulShutdown('SIGTERM')Drain in-flight HTTP, force exit after 10s
SIGINTgracefulShutdown('SIGINT')Same as SIGTERM
unhandledRejectionlogLog only — does not exit
uncaughtExceptionlog + process.exit(1)Log and exit immediately

Shutdown flow for SIGTERM / SIGINT

  1. Log "Received SIGTERM/SIGINT, shutting down gracefully".
  2. Call server.close() — Express stops accepting new connections and drains in-flight requests.
  3. Set a 10-second setTimeout — if server.close() does not complete, log "Forced shutdown after timeout" and call process.exit(1).
  4. On server.close() callback, log "HTTP server closed" and call process.exit(0).

Resources stopped on shutdown

stopMetricsCollectors — clears the 15s setInterval for process gauges. stopRateLimiter — clears the 5-min cleanup interval. stopIdempotencySweeper — clears the 5-min idempotency sweeper. stopDedupCleanup — clears the reputation dedup cleanup. closeLoggerStreams — closes the log file streams.

All setInterval timers are .unref()-ed so they do not block process exit on their own.

Kubernetes shutdown order

Kubernetes sends SIGTERM first, then SIGKILL after terminationGracePeriodSeconds (default 30s). Configure the pod:

YAML example
terminationGracePeriodSeconds: 30

to give the service time to drain. The 10s setTimeout in gracefulShutdown is the inner bound; the 30s K8s limit is the outer bound.