API gateway, usage metering, and billing services for the Callora API marketplace. Talks to Soroban contracts and Horizon for on-chain settlement.
GET /api/developers/mereturns the authenticated developer profile and auto-creates a blank profile row on first access.PATCH /api/developers/meupdates profile fields for the authenticated developer.- PATCH validation enforces a valid
websiteURL and a supportedcategoryenum value.
- Node.js + TypeScript
- Express for HTTP API
- Stellar SDK for Horizon integration
- Circuit Breaker & Retry Patterns for resilience
- Planned: Horizon listener, PostgreSQL, billing engine
- Health check:
GET /api/health - Marketplace routes:
GET /api/apisGET /api/apis/:idPOST /api/apisfor authenticated developers to register an API with priced endpoints
- Usage route:
GET /api/usage - JSON body parsing plus gateway API key authentication for upstream proxy routes
- Per-user global REST rate limiting for authenticated
/api/billing,/api/usage,/api/developers,/api/vault, and/api/keystraffic, with IP fallback for unauthenticated requests - In-memory
VaultRepositorywith:create(userId, contractId, network)findByUserId(userId, network)updateBalanceSnapshot(id, balance, lastSyncedAt)
Gateway proxy routes accept API keys through either:
Authorization: Bearer <api_key>X-Api-Key: <api_key>
The gateway auth middleware performs prefix-based lookup, timing-safe full-key hash verification, revoked-key checks, and request context loading for the authenticated user, vault, api, endpoint, and apiKeyRecord.
See docs/gateway-api-key-auth.md for the full flow, attached request fields, and failure responses.
Authenticated developers can register a marketplace API by calling POST /api/apis with:
{
"name": "Weather API",
"description": "Forecast and current conditions",
"base_url": "https://api.weather.example.com",
"category": "weather",
"endpoints": [
{
"path": "/forecast",
"method": "GET",
"price_per_call_usdc": "0.01",
"description": "Daily forecast"
}
]
}The request requires developer auth via Authorization: Bearer ... or x-user-id in local/test flows. Validation errors return HTTP 400 with field-level details, and successful writes are persisted atomically with their endpoint rows.
- Enforces one vault per user per network.
balanceSnapshotis stored in smallest units using non-negative integerbigintvalues.findByUserIdis network-aware and returns the vault for a specific user/network pair.
PgUsageEventsRepositoryprovides idempotentcreate(...)writes keyed byrequestIdto prevent double billing on retries.- Read methods support time-bounded lookups by
userIdorapiId, plus aggregate totals for user spend and API revenue. - Amounts are handled as smallest-unit
bigintvalues in application code, even though the backing column is namedamount_usdc.
- The runtime now uses PostgreSQL-backed
SettlementStoreandUsageStoreimplementations so/api/developers/revenuesurvives process restarts. - Unsettled usage is persisted through
revenue_ledger, and settlement batches are persisted throughsettlements. - A background revenue ledger indexer backfills
revenue_ledgerfromusage_events, keyed byusage_event_idand resolving API ownership fromapis. - The in-memory store factories are still available for unit tests and isolated local scenarios.
- Apply
migrations/001_create_usage_events.sql,migrations/002_create_settlements.sql,migrations/003_create_revenue_ledger.sql, andmigrations/005_add_persistent_store_columns.sqlbefore starting the API against PostgreSQL.
The backend implements production-grade resilience patterns for Stellar Horizon network calls:
- ✅ Bounded Retry with Exponential Backoff - Automatically retries transient failures
- ✅ Circuit Breaker Pattern - Fast-fails during outages to prevent resource exhaustion
- ✅ Graceful Degradation - Maps upstream failures to appropriate HTTP status codes (502)
- ✅ Health Monitoring - Exposes circuit breaker metrics for observability
See RESILIENCE.md for detailed documentation.
-
Prerequisites: Node.js 18+
-
Install and run (dev):
cd callora-backend npm install -
Configure environment (optional):
cp .env.example .env # Edit .env with your configuration -
Run in development mode:
npm run dev
-
API base:
http://localhost:3000
You can run the entire stack (API and PostgreSQL) locally using Docker Compose:
docker compose up --buildThe API will be available at http://localhost:3000, and the PostgreSQL database will be mapped to local port 5432.
| Command | Description |
|---|---|
npm run dev |
Run with tsx watch (no build) |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run compiled dist/index.js |
npm test |
Run unit tests |
npm run test:coverage |
Run unit tests with coverage |
The dev-only revenue fixture lives in src/data/developerData.ts.
When refreshing it:
- Keep settlement IDs globally unique.
- Keep each settlement under the matching developer key and
developerId. - Use non-negative finite amounts and valid ISO-8601
created_attimestamps. - Keep
tx_hashas eithernullor a non-empty transaction hash forpendingsettlements, and non-empty forcompletedsettlements. - Update usage revenue so fixture summaries stay aligned with the live route semantics:
total_earned = completed + pending + usageandavailable_to_withdraw = usage.
Run npm run lint, npm run typecheck, and npm test after editing the fixture.
The application exposes a standard Prometheus text-format metrics endpoint at GET /api/metrics.
It automatically tracks http_requests_total, http_request_duration_seconds, and default Node.js system metrics.
In production (NODE_ENV=production), this endpoint is protected. You must configure the METRICS_API_KEY environment variable and scrape the endpoint using an authorization header: Authorization: Bearer <YOUR_METRICS_API_KEY>
callora-backend/
|-- src/
| |-- index.ts # Express app and routes
| |-- repositories/
| |-- vaultRepository.ts # Vault repository implementation
| |-- vaultRepository.test.ts # Unit tests
|-- package.json
|-- tsconfig.json
| Variable | Description | Default |
|---|---|---|
PORT |
HTTP port | 3000 |
HORIZON_URL |
Stellar Horizon endpoint | https://horizon-testnet.stellar.org |
STELLAR_BASE_FEE |
Transaction base fee (stroops) | 100 |
STELLAR_TRANSACTION_TIMEOUT |
Transaction timeout (seconds) | 30 |
CIRCUIT_BREAKER_THRESHOLD |
Failures before opening circuit | 5 |
CIRCUIT_BREAKER_COOLDOWN_MS |
Cooldown period (ms) | 30000 |
RETRY_MAX_ATTEMPTS |
Maximum retry attempts | 3 |
RETRY_BASE_DELAY_MS |
Initial retry delay (ms) | 1000 |
See .env.example for complete configuration options.
Run the test suite:
npm testRun with coverage:
npm test -- --coverageThe test suite includes:
- Unit tests for retry mechanism
- Unit tests for circuit breaker
- Integration tests for transaction builder
- HTTP integration tests for controllers
- Mock Horizon responses for various scenarios
Target Coverage: 90%+ line coverage
If the circuit breaker remains open:
- Check
/api/deposits/healthto see current state - Verify
HORIZON_URLis correct and accessible - Wait for cooldown period to elapse
- Restart service to reset circuit breaker
If experiencing high latency:
- Reduce
RETRY_MAX_ATTEMPTS - Lower
CIRCUIT_BREAKER_THRESHOLDto fail faster - Check Horizon service status
- Review logs for retry patterns
See RESILIENCE.md for detailed troubleshooting guide.
Copy .env.example to .env and fill in your values before running locally:
cp .env.example .envThe app validates all environment variables at startup using Zod. If a required variable is missing, the app will exit immediately with a clear error message.
Application errors are returned through the shared Express errorHandler using a consistent JSON envelope:
{
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"requestId": "req_123",
"details": [
{
"field": "query.network",
"message": "Invalid option: expected one of \"testnet\"|\"mainnet\"",
"code": "INVALID_VALUE"
}
]
}codeis a stable machine-readable error code.messageis the user-facing error message.requestIdis the tracing id available to the error handler. When no request id is attached to the Express request, the handler returns"unknown".detailsis included for validation failures and contains field paths such asbody.endpoints[0].pathorquery.network.
For the complete gateway/proxy and billing error-code reference, including 502/504 derivation and Soroban billing mappings, see docs/error-codes.md.
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
HTTP port |
NODE_ENV |
No | development |
development / production / test |
DATABASE_URL |
No | local postgres | Primary PostgreSQL connection string |
DB_HOST |
No | localhost |
Database host |
DB_PORT |
No | 5432 |
Database port |
DB_USER |
No | postgres |
Database user |
DB_PASSWORD |
No | postgres |
Database password |
DB_NAME |
No | callora |
Database name |
DB_POOL_MAX |
No | 10 |
Max pool connections |
DB_IDLE_TIMEOUT_MS |
No | 30000 |
Pool idle timeout (ms) |
DB_CONN_TIMEOUT_MS |
No | 2000 |
Pool connection timeout (ms) |
JWT_SECRET |
Yes | — | Secret for signing JWTs |
ADMIN_API_KEY |
Yes | — | Key for admin endpoints |
METRICS_API_KEY |
Yes | — | Key for /api/metrics in production |
UPSTREAM_URL |
No | http://localhost:4000 |
Gateway upstream URL |
PROXY_TIMEOUT_MS |
No | 30000 |
Proxy request timeout (ms) |
REST_RATE_LIMIT_WINDOW_MS |
No | 60000 |
Window length for REST API rate limiting (ms) |
REST_RATE_LIMIT_MAX_REQUESTS |
No | 100 |
Max REST API requests allowed per user/IP per window |
CORS_ALLOWED_ORIGINS |
No | http://localhost:5173 |
Comma-separated allowed origins |
SOROBAN_RPC_ENABLED |
No | false |
Enable Soroban RPC health check |
SOROBAN_RPC_URL |
If SOROBAN_RPC_ENABLED=true |
— | Soroban RPC endpoint URL |
SOROBAN_RPC_TIMEOUT |
No | 2000 |
Soroban RPC timeout (ms) |
HORIZON_ENABLED |
No | false |
Enable Horizon health check |
HORIZON_URL |
If HORIZON_ENABLED=true |
— | Horizon endpoint URL |
HORIZON_TIMEOUT |
No | 2000 |
Horizon timeout (ms) |
SETTLEMENT_STATUS_SYNC_INTERVAL_MS |
No | 60000 |
Settlement-status sync polling interval (ms) |
SETTLEMENT_STATUS_SYNC_TIMEOUT_MS |
No | 5000 |
Per-request Horizon timeout for settlement sync (ms) |
HEALTH_CHECK_DB_TIMEOUT |
No | 2000 |
DB health check timeout (ms) |
APP_VERSION |
No | 1.0.0 |
Reported in health check responses |
LOG_LEVEL |
No | info |
trace / debug / info / warn / error / fatal |
GATEWAY_PROFILING_ENABLED |
No | false |
Enable request profiling |
GET /api/health reports per-dependency status when detailed health checks are enabled:
checks.databasefor PostgreSQLchecks.soroban_rpcfor Soroban RPC whenSOROBAN_RPC_ENABLED=truechecks.horizonfor Horizon whenHORIZON_ENABLED=true
Each dependency uses its own bounded timeout, so a hung database or remote Stellar service cannot stall the full health response. Use HEALTH_CHECK_DB_TIMEOUT for PostgreSQL, SOROBAN_RPC_TIMEOUT for Soroban RPC, and HORIZON_TIMEOUT for Horizon.
- The server listens for
SIGTERMandSIGINTand performs a graceful shutdown. - On shutdown, it stops accepting new HTTP requests, drains in-flight
/v1/callproxy work, waits for active webhook deliveries to finish, and then closes database resources. - A 30 second timeout is enforced for in-flight connections; lingering sockets are destroyed to prevent hung termination.
- Background workers should stop scheduling new runs as soon as shutdown begins and finish any in-flight work inside the same drain window.
- Shutdown hooks are registered with
process.once(...)to avoid duplicate execution during restarts. - The dev workflow (
npm run devwithtsx watch) is preserved. Restarts trigger the same graceful path instead of abrupt termination.
Set one active network per deployment. The backend reads STELLAR_NETWORK first, then SOROBAN_NETWORK as a fallback.
# Select exactly one active network per deployment
STELLAR_NETWORK=testnet # or: mainnetPer-network values:
# Testnet values
STELLAR_TESTNET_HORIZON_URL=https://horizon-testnet.stellar.org
SOROBAN_TESTNET_RPC_URL=https://soroban-testnet.stellar.org
STELLAR_TESTNET_VAULT_CONTRACT_ID=CC...TESTNET_VAULT
STELLAR_TESTNET_SETTLEMENT_CONTRACT_ID=CC...TESTNET_SETTLEMENT
# Mainnet values
STELLAR_MAINNET_HORIZON_URL=https://horizon.stellar.org
SOROBAN_MAINNET_RPC_URL=https://soroban-mainnet.stellar.org
STELLAR_MAINNET_VAULT_CONTRACT_ID=CC...MAINNET_VAULT
STELLAR_MAINNET_SETTLEMENT_CONTRACT_ID=CC...MAINNET_SETTLEMENT
# Optional transaction builder overrides
STELLAR_BASE_FEE=100
STELLAR_TRANSACTION_TIMEOUT=300
SETTLEMENT_STATUS_SYNC_INTERVAL_MS=60000
SETTLEMENT_STATUS_SYNC_TIMEOUT_MS=5000Notes:
- Do not point a testnet deployment at mainnet URLs or contract IDs (or vice versa).
- Deposit transaction building uses the configured network Horizon URL and validates vault contract ID when configured.
- Deposit transaction building defaults to a
100stroop fee and a300second timeout unless overridden. - Soroban settlement client uses the configured network RPC URL and settlement contract ID.
GET /api/vault/balanceaccepts an optionalnetworkquery param.- Accepted values are
testnetandmainnet. - When omitted, the route defaults
networktotestnet. - Invalid values are rejected consistently with a
400validation response.
This repo is part of Callora:
- Frontend:
callora-frontend - Contracts:
callora-contracts