Skip to content

About

service that brokers swap intents between users and the solver network and streams the live intent feed. Part of the multi-repo Vortex stack

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

556 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vortex-backend

Intent relay API + WebSocket feed for Vortex Protocol.

CI License: MIT

TypeScript / NestJS service that brokers swap intents between users and the solver network and streams the live intent feed. Part of the multi-repo Vortex stack — see also vortex-contract and vortex-frontend.

Intents persist to Postgres in production (INTENTS_STORE=postgres); local development defaults to an in-memory store seeded with mock data. Read-only Soroban RPC access is live (/api/v1/chain/*); writing intent state on-chain is still on the roadmap.

Rebuild complete: the service has been ported from Express to NestJS. All endpoints below are live.


API Endpoints

GET  /api/v1/intents              — list intents (filter by state, user, chain)
GET  /api/v1/intents/open         — all open intents (solver view)
GET  /api/v1/intents/:id          — single intent
GET  /api/v1/intents/user/:addr   — intents for a user
POST /api/v1/intents              — create intent (oracle-checked minDstAmount; 201 includes fairValue + slippageBps)
POST /api/v1/intents/:id/accept   — solver accepts
POST /api/v1/intents/:id/fill     — solver fills
POST /api/v1/intents/:id/cancel   — user cancels
POST /api/v1/intents/quote        — get best quote from solvers
GET  /api/v1/solvers              — solver leaderboard
GET  /api/v1/solvers/:addr/stats  — solver performance stats
GET  /api/v1/tokens               — supported tokens (filter by chain)
POST /api/v1/admin/tokens         — register a token (admin key, on-chain metadata check)
PATCH /api/v1/admin/tokens        — update status or re-verified metadata
DELETE /api/v1/admin/tokens       — soft-delist a token (existing intents keep working)
GET  /api/v1/stats                — protocol stats
GET  /api/v1/datasets             — published dataset dates, revisions, and schemas
GET  /api/v1/datasets/latest      — most recent dataset publication (404 if none)
GET  /api/v1/datasets/schemas     — versioned dataset schemas
GET  /health                      — service health
GET  /health/timeline             — readiness/indicator transition history (internal/ops)
WS   /ws                          — real-time intent feed
GET  /docs                        — Swagger / OpenAPI docs
GET  /docs/v1                     — OpenAPI UI for API v1
GET  /docs/v2                     — OpenAPI UI for API v2
GET  /api/v1/chain/health         — Soroban RPC health (read-only)
GET  /api/v1/chain/ledger         — latest Soroban ledger
GET  /api/v1/chain/network        — Soroban network info
GET  /api/v1/chain/account/:key   — Stellar account lookup
POST /api/v1/api-keys/signup      — public self-service signup (email); always issues an `integrator`-tier key
GET  /api/v1/api-keys/signup/confirm — complete signup via emailed token; returns the plaintext key exactly once
GET  /api/v1/admin/api-keys/:id/usage — per-key request volume (bucketed, allowed vs 429'd; `?from=&to=`)
GET  /api/v1/admin/credentials/expiring-soon — API keys / solver credentials expiring within the warning window

Health timeline

GET /health/timeline?indicator=&since= returns the append-only history of readiness and per-indicator status transitions recorded by HealthIndicatorRegistry — one row per actual flip (indicator, fromStatus, toStatus, at, optional error), not one per health-check tick. Use it to answer "when did soroban_rpc_quorum go down, and for how long" during an incident retro without grepping logs across replicas.

  • indicator — optional filter, e.g. soroban_rpc_quorum (omit for all).
  • since — optional ISO-8601 timestamp; only transitions at or after it.

Storage is selected with HEALTH_TIMELINE_PERSISTENCE: memory (default) keeps the last 500 transitions in a bounded in-process ring buffer, so history is per-replica and lost on restart; prisma persists transitions to Postgres for durable, cross-replica history. Recording a transition is a plain in-memory array push — no I/O is added to the health-check path.

API version lifecycle

HTTP API versions use Nest URI versioning and retain the /api/vN/... URL shape. /docs/v1-json and /docs/v2-json serve version-specific OpenAPI documents; the legacy /docs-json remains available as the combined document. New controller versions are selected with Nest's controller version metadata.

Deprecation headers are enabled per version by setting API_V1_DEPRECATED_AT to an ISO-8601 date. API_V1_SUNSET_AT adds the corresponding Sunset header, and API_V1_DEPRECATION_LINK adds a Link header with rel="deprecation". These headers are omitted until configured. HTTP request counters and latency histograms include a version label (v1, v2, or unversioned).


Local Development

Prerequisites

  • Node.js 20+
  • Docker (optional, for the one-command local Postgres + app dev stack)
npm install
cp .env.testnet.example .env   # testnet development (most contributors)
# cp .env.mainnet.example .env # production/mainnet — requires real keys
npm run dev    # http://localhost:4000

One-command local stack with Postgres

If you need the Prisma-backed local database flow, use the repo-provided compose stack:

docker compose up --build

This starts:

  • a postgres:16-alpine service matching the CI credentials (vortex / vortex / vortex)
  • the app service built from the existing Dockerfile
  • the app already pointed at DATABASE_URL=postgresql://vortex:vortex@postgres:5432/vortex?schema=public

After the stack is up, the backend is available at http://localhost:4000 and the DB is reachable using the same default credentials shown in .env.example.

Three .env.example variants are provided for different deployment targets:

File Use case
.env.example Generic template with all available variables
.env.testnet.example Testnet development — safe defaults, blank contract IDs
.env.mainnet.example Production mainnet — strict CORS, required signing key and contract IDs

Admin API keys

ADMIN_API_KEYS is a comma-separated list of admin credentials in the format:

label:role:secret[:expiresAtIso]
  • label — human-readable name for the key (e.g. ops, auditor).
  • role — the admin role granted to the key.
  • secret — the bearer secret presented by the caller.
  • expiresAtIso — optional ISO-8601 timestamp after which the key stops authenticating. A 3-field entry with no expiry behaves exactly as before (valid indefinitely).

Expired keys are treated exactly like unknown secrets: authenticateAdminKey() returns null and AdminGuard responds with the same 401 Unauthorized as a missing or invalid key, so expiry cannot be used to probe for valid-but-expired secrets. No background sweep is needed — expiry is checked once per request.

Entries whose expiresAtIso is malformed or already in the past at startup are dropped entirely by parseAdminApiKeys(), as if they had never been configured, so a bad config can never admit an expired key through another code path.

# permanent key + a temporary auditor key that self-expires
ADMIN_API_KEYS="ops:admin:s3cret,auditor:readonly:audit-s3cret:2025-12-31T23:59:59Z"

Signing key

SOROBAN_SIGNING_KEY holds the secret key the backend uses to submit its own on-chain writes (settlement, slashing). It's optional in development/test — leave it blank and those code paths simply have nothing to sign with — but required and format-validated in production (NODE_ENV=production); the process refuses to start without a well-formed Stellar secret seed rather than falling back to any placeholder.

For local dev, generate a throwaway testnet-only keypair — never reuse a mainnet or otherwise real key:

npx @stellar/stellar-cli keys generate local-dev --network testnet
npx @stellar/stellar-cli keys show local-dev        # paste into SOROBAN_SIGNING_KEY

# or, ad hoc:
node -e "console.log(require('@stellar/stellar-sdk').Keypair.random().secret())"

# fund it via Friendbot before using it against testnet:
curl "https://friendbot.stellar.org/?addr=<PUBLIC_KEY>"

Never commit a filled-in .env, and never point a real/funded key at anything but mainnet with NODE_ENV=production behind a proper secrets manager.

Scripts

Script Description
npm run dev Watch-mode Nest server (nest start --watch)
npm run build Build the Nest app (dist/)
npm run start Run compiled server (dist/main.js)
npm run lint ESLint over src
npm run typecheck tsc --noEmit
npm run test Run the unit test suite
npm run test:e2e Run the e2e test suite (supertest against a real booted app)
npm run solver:demo Run the reference solver bot (scripts/README.md)

Docker

docker build -t vortex-backend .
docker run -p 4000:4000 vortex-backend

No .env is required to boot — ConfigModule's validation schema supplies defaults for every variable. Pass real values with --env-file .env or -e flags to override them.

Production deployment

The table below separates variables that must be set for a real deployment from those that are safe to leave at their testnet/dev defaults.

Variable Required for production Safe default? Notes
NODE_ENV Yes — set to production No Enables signing-key validation and strict startup checks
DATABASE_URL Yes No Must point to a managed Postgres instance, not localhost
CORS_ORIGIN Yes No Must be an explicit origin (e.g. https://app.vortex.trade), never *
SOROBAN_SIGNING_KEY Yes (once on-chain writes land) No Required and format-validated in production; process refuses to start without a valid Stellar secret seed
SETTLEMENT_CONTRACT_ID Yes (on-chain path) No 56-char Stellar contract ID of the deployed settlement contract
SOLVER_REGISTRY_CONTRACT_ID Yes (on-chain path) No 56-char Stellar contract ID of the deployed solver-registry contract
STELLAR_NETWORK Yes No Set to mainnet; default is testnet
SOROBAN_RPC_URL Yes No A production-grade Soroban RPC endpoint; the default points at the public testnet
INTENTS_STORE Yes — set to postgres memory memory loses all intents on restart and cannot scale horizontally. Promote via dual per docs/runbooks/intents-store-migration.md. INTENTS_PERSISTENCE=prisma is a deprecated alias for postgres
SOLVERS_PERSISTENCE Recommended memory Set to prisma to persist solver registry to Postgres; memory loses solver state on restart
EVM_DEPOSIT_VERIFICATION_ENABLED Yes — set to true false EVM-source intents are hidden from solvers until the escrow deposit is confirmed; needs EVM_RPC_URLS and EVM_ESCROW_ADDRESSES. See docs/runbooks/evm-deposit-verification.md
TTL_KEEPER_ENABLED Yes (on-chain path) false Extends the TTL of the contract instances, WASM code, active intents and solver bonds before they archive. Budget-capped by TTL_KEEPER_DAILY_BUDGET_STROOPS. See docs/runbooks/contract-ttl.md
SOROBAN_FEE_PERCENTILE Recommended p50 Raise to p90 on mainnet for better confirmation speed under load
WS_MAX_CONNECTIONS Recommended 1000 Tune to expected solver + frontend connection count
SENTRY_DSN Recommended — (Sentry disabled) Set to your Sentry project DSN for error alerting
LOG_LEVEL Recommended debug Set to info in production — debug is too noisy
LEADER_ELECTION_ENABLED Recommended (multi-replica) false Set to true when running N > 1 replicas to ensure singleton workers run on exactly one pod. Requires DATABASE_URL to point at a live Postgres instance. Do not use PgBouncer in transaction-pooling mode — see Leader Election runbook.
LEADER_ELECTION_HEARTBEAT_MS Optional 5000 Heartbeat interval in ms. Lower = faster failover, higher DB load. Default gives ≤ 15 s failover.
HEALTH_TIMELINE_PERSISTENCE Optional memory Where HealthIndicatorRegistry transition history is stored. memory keeps a bounded in-process ring buffer (last 500 transitions, per replica); prisma persists transitions to Postgres for cross-replica/durable history. See GET /health/timeline.
PORT Optional 4000 Change if the container port mapping differs
MAX_USER_SLIPPAGE_BPS Optional 100 Max user slippage vs oracle fair minDstAmount (1% default). Higher slippage requires a signed acknowledgeHighSlippage.
MAX_PREMIUM_BPS Optional 50 Max minDstAmount premium above oracle fair value; always rejected above this.
ORACLE_FAIL_OPEN_MAX_USD Optional 100 When oracle prices are missing/stale, intents with source notional at or below this USD amount are still created.
ORACLE_MAX_STALENESS_MS Optional 60000 Price snapshots older than this are treated as unavailable.

For a production .env template, copy .env.mainnet.example — every <CHANGE_ME> value corresponds to a "required for production" row above. Store secrets (SOROBAN_SIGNING_KEY, DATABASE_URL) in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) and inject them at runtime; never commit filled-in values to version control.

Verified security headers

The Nest app boots with Helmet enabled and explicitly configures HSTS for HTTPS origins. The backend also trusts a single proxy hop (app.set("trust proxy", 1)) so a TLS-terminating load balancer can pass through X-Forwarded-Proto: https and allow Helmet/HSTS to emit Strict-Transport-Security rather than silently skipping it behind a proxy.

The HTTP response set is verified in the e2e suite to include Helmet defaults such as X-Content-Type-Options: nosniff alongside the configured HSTS policy. This is the baseline transport-security posture the service relies on in production.


Supported chains

SupportedChain in src/intents/intents.types.ts lists seven chains. The table below clarifies which are live (real integration exists today) versus planned (schema/token data in place, on-chain settlement pending).

Chain Status Notes
Stellar ✅ Live Soroban RPC reads (/api/v1/chain/*), signing service, settlement design in progress
Ethereum 🔲 Planned Token registry populated; on-chain integration not yet implemented
Base 🔲 Planned Token registry populated; on-chain integration not yet implemented
Polygon 🔲 Planned Token registry populated; on-chain integration not yet implemented
Arbitrum 🔲 Planned Token registry populated; on-chain integration not yet implemented
Optimism 🔲 Planned Token registry populated; on-chain integration not yet implemented
Avalanche 🔲 Planned Token registry populated; on-chain integration not yet implemented

Contributor note: EVM chains are accepted in the intent DTO. With EVM_DEPOSIT_VERIFICATION_ENABLED=true the backend confirms the user's escrow deposit (src/chains/evm/) before the intent is offered to solvers; no settlement or bridging logic is wired up yet. See docs/architecture/onchain-settlement.md for the target design.


Roadmap

  • Soroban RPC reads — health/ledger/network/account lookups via /api/v1/chain/*
  • Durable intent store — intents persist to Postgres (INTENTS_STORE=postgres) with atomic SQL transitions, optimistic concurrency (ETag / If-Match) and cross-replica idempotency; migration via a dual-write phase (runbook)
  • On-chain writes — back intent state transitions with real Soroban transactions (target design: docs/architecture/onchain-settlement.md)
  • Solver WS client — reference implementation for a solver bot (npm run solver:demo, see scripts/README.md)

Performance Testing (k6)

The repo ships a k6 performance suite that gates PRs against latency and error-rate budgets. Results are posted automatically as a PR comment and uploaded as CI artifacts.

Scenarios

Scenario File What it exercises
create-intent-burst scenarios/create-intent-burst.js POST /api/v1/intents burst (20 VUs, 80 s)
solver-polling scenarios/solver-polling.js GET /api/v1/intents/open polling (10 VUs, 60 s)
quote-requests scenarios/quote-requests.js POST /api/v1/intents/quote ramping arrival rate
mixed-lifecycle scenarios/mixed-lifecycle.js Full read/write cycle (create → poll → read → quote → stats)

All four run concurrently via test/perf/k6/all-scenarios.js in CI.

Baseline budgets

Budgets live in test/perf/k6/baselines/all-scenarios.json. PRs that regress any metric beyond +15% of baseline fail the k6-perf job.

Metric p95 budget p99 budget
http_req_duration (all) 500 ms 1000 ms
POST /intents 200 ms 400 ms
GET /intents/open 150 ms 300 ms
POST /intents/quote 200 ms 500 ms
Full lifecycle cycle 600 ms —
Read-only endpoints 150 ms —
Global error rate < 1% —

Running locally

Prerequisites: k6 installed and the server running (npm run dev).

# Run all four scenarios (CI entry point)
npm run perf

# Run a single scenario
npm run perf:create-intent
npm run perf:solver-polling
npm run perf:quote-requests
npm run perf:mixed-lifecycle

# Compare the latest run against baselines
npm run perf:compare

# Update baselines after a known-good run on main
npm run perf:update-baselines

# Regenerate pre-computed Ed25519 fixture signatures
npm run perf:gen-fixtures

Noise control

The CI workflow runs k6 three times and uses the last run's summary. The +15% tolerance band absorbs run-to-run variation on the fixed-size GitHub Actions runner. Baselines are automatically updated on main pushes, so the reference point always tracks the current head.

Updating budgets

Edit the thresholds section in test/perf/k6/baselines/all-scenarios.json. Do not edit reference — that section is overwritten automatically by npm run perf:update-baselines.


Preview environments

Every same-repo PR can be deployed to an isolated, seeded preview environment (API, Swagger at /docs, WebSocket at /ws) by adding the preview label; it is torn down on close, on label removal, or after a TTL. See docs/ci/preview-environments.md for the trigger rules, cost cap and required repository configuration. Previews are testnet-only with throwaway keys — forked PRs are excluded.

Contributing

See CONTRIBUTING.md for backend-specific setup, conventions, and the PR checklist. For community expectations and governance, see CODE_OF_CONDUCT.md and docs/rfcs/README.md.

License

MIT © 2025 Vortex Protocol Contributors

About

service that brokers swap intents between users and the solver network and streams the live intent feed. Part of the multi-repo Vortex stack

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages