Off-chain services for Refract — oracle monitoring, automatic claim processing, and premium quoting.
This service watches real-world data feeds, pushes readings to the on-chain
RefractOracle, scans active policies for triggered conditions, and exposes a
REST + WebSocket API the web app consumes. See also refract-contracts and
refract-frontend.
- Express REST API + ws WebSocket feed
- PostgreSQL for policy / claim / pool snapshots (
src/db/schema.sql) - Redis (ioredis) for caching & pub-sub
- @stellar/stellar-sdk for Soroban interaction
- Zod for request validation, Winston for logging
src/
├── index.ts # Express + WebSocket server, service loop
├── services/
│ ├── oracleMonitor.ts # polls data sources (depeg, crash, TVL, flight)
│ └── claimProcessor.ts # scans policies, settles triggered claims
├── routes/
│ ├── quotes.ts # POST /quote, GET /coverage-types
│ ├── policies.ts # policy CRUD + buy-tx builder
│ └── pool.ts # pool stats, provide/withdraw
└── db/schema.sql # PostgreSQL schema
Start the backend and its PostgreSQL and Redis dependencies with Docker Compose; Docker Compose initializes the database schema on first startup:
docker compose up --buildSTELLAR_NETWORK selects testnet, futurenet, or mainnet; the matching
Soroban RPC URL, Horizon URL, and network passphrase are selected together.
Set the network-suffixed contract IDs for the selected network (for example,
REFRACT_POOL_CONTRACT_ID_TESTNET) rather than overriding endpoint or
passphrase values independently. The backend checks the RPC passphrase and,
when a pool contract ID is configured, validates the live pool WASM contract
spec at startup. Startup fails with the specific interface mismatch if the
deployed contract differs from the backend's assumed interface.
For an existing database, apply
src/db/migrations/001_soroban_pool_events.sql to add idempotent storage and
checkpoints for Soroban pool events. Once REFRACT_POOL_CONTRACT_ID is set,
the backend polls the configured Soroban RPC endpoint every 10 seconds and
stores policy purchases, capital deposits/withdrawals, and settled claims in
soroban_pool_events. Set SOROBAN_EVENT_START_LEDGER to an RPC-retained
ledger for the first run; subsequent progress is checkpointed in Postgres.
The API listens on http://localhost:4001. PostgreSQL and Redis are also
available on localhost ports 5432 and 6379. Compose uses the development-only
Postgres password refract_dev unless POSTGRES_PASSWORD is set in the
environment or .env; configure secrets and production service settings
separately before deployment. Contract IDs and the relayer key can be added to
.env when exercising configured on-chain operations.
To run the API directly on the host, start PostgreSQL and Redis yourself,
copy .env.example to .env, install Node.js 22 dependencies, apply
src/db/schema.sql once, and run npm run dev.
Stop the stack with docker compose down. Persistent database and Redis data
remain in Docker volumes; docker compose down -v removes them.
| Command | Purpose |
|---|---|
npm run dev |
Hot-reloading dev server (ts-node + nodemon) |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run the compiled server |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint over src/ |
Interactive OpenAPI documentation and the generated JSON document are served
at /api/docs and /api/docs-json.
Schemas are generated from the Nest controllers and request DTOs.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness probe |
GET |
/metrics |
Prometheus-format HTTP, scheduler, oracle, and Soroban RPC metrics |
GET |
/api/v1/quotes/coverage-types |
List coverage types & rates |
POST |
/api/v1/quotes/quote |
Quote a premium |
GET |
/api/v1/policies/holder/:address |
Policies for a holder |
POST |
/api/v1/policies/buy |
Build a buy-policy transaction |
GET |
/api/v1/pool/stats |
Pool capital / utilization / APY |
GET |
/api/v1/pool/positions |
Paginated LP positions sorted by committed capital |
POST |
/api/v1/pool/provide · /withdraw |
LP capital flows |
GET |
/api/v1/claims/dead-letter |
Triggered claims withheld after repeated failed settlement |
POST |
/api/v1/tx/submit |
Submit signed XDR; confirmed policy buys return their on-chain ID |
WS |
/ |
Live oracle alert stream |
State-changing API attempts emit structured security_audit JSON lines to
stdout; route those events to the deployment's log aggregation or SIEM system.
See SECURITY.md for the event fields and identity limitations.
Endpoints with the @AdminOnly() guard require X-API-Key: $ADMIN_API_KEY.
Set a strong, private ADMIN_API_KEY before enabling admin-only routes; if
it is unset, those routes fail closed with 503. The current API has no
admin CRUD, dead-letter review, or contract-override routes. API request
limits are applied per client IP and endpoint: 30/minute for quotes,
10/minute for policy buys, LP actions, oracle status, and on-chain coverage
reads, and 60/minute for coverage catalogs.
⚠️ Oracle data sources:StablecoinDepeg,MarketCrash, andSmartContractRisknow call real, keyless public APIs — CoinGecko (USDC/XLM price), Stellar Horizon testnet (chain context), and DeFiLlama (protocol TVL). No API key is required for any of them.LiquidationShieldandFlightDelaystay mocked: there's no public API for NEXUS Protocol liquidation events, and AviationStack (flight data) requires a paid key this project doesn't have. Seesrc/oracle/oracle.service.tsfor details. Claim settlement invokespool.process_claim(policy_id: u64)viaClaimSettlementService— a single u64 on-chain policy id (holder/payout are looked up on-chain). Policies startpendingafterPOST /buy; once the holder submits the signed XDR throughPOST /api/v1/tx/submit, the buy_policy return value is stored asonChainPolicyIdand the policy becomesactive(only active policies are claim-scanned). Settlement refuses a null on-chain id without entering the retry loop. The signing key is fetched at runtime from AWS Secrets Manager using the workload's AWS credentials; configureORACLE_RELAYER_SECRET_IDand grant the runtime IAM rolesecretsmanager:GetSecretValueon that secret. The contract's exact function signature is an unverified best-effort guess — this repo doesn't include therefract-contractssource, so it needs confirmation against the real deployed contract; seesrc/claim/claim-settlement.service.tsfor details. A policy only deactivates once settlement actually confirms on-chain — a failed or unconfirmed payout leaves it active for the next scheduled retry.Claim settlement retries failed submissions for up to three scheduled scans, then exposes the claim and last error through
/api/v1/claims/dead-letterand stops retrying it automatically. This dead-letter list is currently in memory and is cleared on process restart. The knownprocess_claimargument mismatch is checked against the deployed contract spec at startup; this check intentionally prevents startup until the backend call matches the deployed interface. Seesrc/claim/claim-settlement.service.tsandsrc/stellar/pool-contract-interface.service.ts.Relayer account sequences are serialized across replicas using PostgreSQL advisory locks; during key rotation, roll out the new key while old replicas drain.
Every transaction signed by the relayer is also recorded in the dedicated PostgreSQL
relayer_transaction_audittable with its lifecycle status, transaction hash, signing key's public address, and policy ID. Apply the currentsrc/db/schema.sqlbefore enabling relayer settlement.Oracle publisher (
OraclePublisherService) pushes readings toREFRACT_ORACLE_CONTRACT_IDviaupdate_reading(u32, i128)when the relayer secret is set. Degraded fail-safe readings are never published. Publish cadence is controlled byORACLE_PUBLISH_MODE/ORACLE_PUBLISH_MIN_INTERVAL_MS(see.env.example).Confirmation polling uses configurable exponential backoff + jitter (
CONFIRMATION_*env vars) with distinct HTTP vs settlement deadlines.Before building a caller-signed policy purchase or pool-capital XDR, the backend checks via Horizon that the caller's account exists and has a positive native XLM balance, and returns a funding instruction if not. This README predates the NestJS migration in some other places (route layout, stack description) — a fuller pass is pending; see
CONTRIBUTING.md.