This backend is the off-chain metering and billing mirror for AgentPay. It
accepts usage events, keeps in-memory counters by agent and service, quotes
stroop-denominated bills, and drains counters when POST /api/v1/settle is
called. It does not move Stellar value by itself; on-chain settlement remains a
separate integration point.
src/index.ts builds the Express app with createApp() and mounts feature
routers after the shared middleware chain:
installPreRouteMiddleware(app)installs CORS, JSON parsing, security headers, and request-id handling.- Early operational routers mount admin, config, and metrics routes.
installRequestStateMiddleware(app)recognizes known API keys, enforces the pause guard for writes, applies the in-process rate limiter, and records request timing logs.- Feature routers mount metadata, usage, service registry, API key, event, and webhook routes.
installErrorHandlers(app)normalizes terminal failures.
When the compiled entrypoint is run directly, src/index.ts starts the HTTP
server and installs SIGTERM/SIGINT handlers. The shutdown handler calls
server.close() and forces exit after a ten-second drain timeout.
All current state lives in process memory under src/store/state.ts:
usageStore: mapsusageKey(agent, serviceId)to the outstanding request count for that pair.usageKeyuses the${agent}::${serviceId}shape.servicesStore: mapsserviceIdto service pricing metadata, currently{ priceStroops }.servicesMetadata: mapsserviceIdto optional descriptive metadata such asdescriptionandowner.servicesDisabled: tracks services blocked from new usage recording while preserving historical counters.apiKeyStore,webhooks,rateBuckets,pauseState, andruntimeConfighold local operational state for adjacent features.eventLoginsrc/events.tsis a bounded audit log. TherecordEventhelper appends events such asusage.recorded,usage.settled, andwebhook.test.
Because these stores are in memory, a process restart clears usage counters, service registrations, API keys, webhook registrations, rate-limit buckets, and audit events. A durable store should replace or back these maps before this backend is used as the source of truth for production settlement.
sequenceDiagram
participant Client
participant Middleware
participant Router
participant Store
participant Events
Client->>Middleware: HTTP request
Middleware->>Middleware: CORS, JSON, security headers
Middleware->>Middleware: X-Request-Id, API-key recognition
Middleware->>Middleware: pause guard, rate limit, timing
Middleware->>Router: validated Express request context
Router->>Store: read/write in-memory state
Router->>Events: recordEvent(...) when auditable
Router-->>Client: JSON response with X-Request-Id
GET, HEAD, and OPTIONS requests continue while pauseState.paused is
true. State-changing requests are rejected with 503 service_paused except for
POST /api/v1/admin/unpause.
The core off-chain lifecycle is:
- Register a service with
POST /api/v1/services. - Record usage with
POST /api/v1/usage. - Quote the outstanding bill with
GET /api/v1/billing/:agent/:serviceId. - Drain the counter with
POST /api/v1/settle. - Use a future settlement job to connect the drained quote to an on-chain payment or contract event.
sequenceDiagram
participant Agent
participant Backend
participant Store as usageStore/servicesStore
participant Events as eventLog
participant Chain as Future Stellar settlement
Agent->>Backend: POST /api/v1/usage {agent, serviceId, requests}
Backend->>Store: usageKey(agent, serviceId) += requests
Backend->>Events: recordEvent("usage.recorded", ...)
Backend-->>Agent: 201 {agent, serviceId, total}
Agent->>Backend: GET /api/v1/billing/:agent/:serviceId
Backend->>Store: read requests and priceStroops
Backend-->>Agent: 200 {requests, priceStroops, billedStroops}
Agent->>Backend: POST /api/v1/settle {agent, serviceId}
Backend->>Store: read requests, compute billedStroops
Backend->>Store: set usageKey(agent, serviceId) to 0
Backend->>Events: recordEvent("usage.settled", ...)
Backend-->>Agent: 200 {requests, priceStroops, billedStroops}
Backend-->>Chain: planned job submits or verifies payment
POST /api/v1/settle is deliberately an accounting drain. It mirrors the
shape of an on-chain settlement calculation by multiplying outstanding requests
by priceStroops, but the route does not submit a Stellar transaction, escrow
funds, or prove payment. Consumers should pair settlement responses with a
contract transaction or ledger event before marking an invoice paid.
The current API is open for local development and demos:
- API keys can be created and recognized, but most routes do not require a key.
- The pause guard is operational protection, not authentication.
- Request IDs support correlation and auditability, not authorization.
- CORS is allowlist-based only when
CORS_ALLOWED_ORIGINSis configured. - Rate limiting is in-process and IP-based, so it resets on restart and is not shared across replicas.
Production deployments should add durable authentication, authorization, shared rate limiting, persistent storage, and a verified settlement worker before using the backend as a payment control plane.
The natural durability boundary is the set of maps in src/store/state.ts and
the bounded eventLog in src/events.ts. A future store adapter should keep the
public route contracts unchanged while persisting:
- usage counters keyed by
usageKey(agent, serviceId); - registered services, disabled state, and service metadata;
- audit events emitted by
recordEvent; - API keys and webhook subscriptions.
A future settlement job should consume drained settlement records or audit events, submit or verify the Stellar-side payment, then attach the transaction result to the backend audit trail. Until that exists, the backend remains a metering and quote service rather than an on-chain payment executor.