Prediction market API. auth, security, KYC and more.
- Node.js + TypeScript
- Fastify
- PostgreSQL
- Drizzle ORM
- user registration with password identity
- password login with opaque session tokens
- idle-expiring bearer sessions with explicit session management
- linking a password identity to an existing user record
- suspicious login auditing with temporary restriction thresholds
- scoped API key creation for authenticated users
- compliance capability evaluation and review holds
- funding method registry with ledger-backed wallet balances
- public market catalog and market detail bootstrap
- authenticated order intake with idempotent reservation
- Copy
.env.exampleto.env. - Install dependencies with
npm install. - Generate or review migrations with
npm run db:generatewhen the schema changes. - Apply the current migration set with
npm run db:migrate. - Run the server with
npm run dev.
If you need browser-based clients such as the Scalar docs preview to call the API from a different origin, set CORS_ALLOWED_ORIGINS to a comma-separated allowlist. In development, Hyperwood also accepts localhost and private-network origins by default so WSL-hosted docs previews can reach the API.
Primary currency behavior is configurable:
PRIMARY_MARKET_CURRENCYcontrols the default currency used when external requests omitcurrencySUPPORTED_MARKET_CURRENCIEScontrols which market/account currencies the API accepts at runtime
The current database schema stores market, order, and exchange-fee currencies as varchar(3) so adding a new code no longer requires a PostgreSQL enum change.
Hyperwood applies default external API throttles:
- public auth routes:
AUTH_RATE_LIMIT_MAX_REQUESTSwithinAUTH_RATE_LIMIT_WINDOW_SECONDS - other external API routes:
API_RATE_LIMIT_MAX_REQUESTSwithinAPI_RATE_LIMIT_WINDOW_SECONDS
Exceeded windows return 429 rate_limit_exceeded and are available to operators through the internal rate-limit event feed.
Every HTTP response also includes X-Request-Id. Clients may send x-request-id to preserve their own correlation identifier across API logs and support workflows.
Verification email delivery is configurable:
EMAIL_DELIVERY_PROVIDERselectsdevelopment_overrideormailersendEMAIL_FROM_NAMEcontrols the sender display nameMAILERSEND_API_TOKENauthenticates MailerSend API requestsMAILERSEND_DOMAINidentifies the verified MailerSend domainMAILERSEND_FROM_EMAILmust use the configured MailerSend domain
In development and test, development_override keeps the raw verification token in the API response for local workflows. Production deployments should switch to mailersend.
- Copy
.env.exampleto.env. - Start the stack with
npm run docker:local:up. - The API will be available at
http://localhost:3000and PostgreSQL atlocalhost:5432.
The local Docker stack uses docker-compose.local.yml, starts PostgreSQL 17, overrides DATABASE_URL to the internal db service, and runs npm run db:migrate before the development server starts.
The same stack also runs the Docusaurus developer-doc app on http://localhost:3001.
- Copy
.env.exampleto.env. - Start the stack with
npm run docker:prod:up. - The API will be available at
http://localhost:${API_PORT:-3000}and PostgreSQL atlocalhost:${POSTGRES_PORT:-5432}.
The production stack uses docker-compose.yml, starts only db and api, runs database migrations on container start, builds the TypeScript app inside the container image, and then runs npm start.
The API documentation source lives in:
docs/openapi/openapi.yamldocs/openapi/paths/docs/openapi/components/docs/guides/docs/developer/
OpenAPI is the source of truth for HTTP reference documentation. Developer onboarding, architecture, business rules, and module documentation now live in the Docusaurus app under docs/developer. Narrower integration guides still live under docs/guides and are mounted inside that Docusaurus site.
The interactive API reference is rendered with Scalar.
Useful commands:
npm run lintnpm run lint:fixnpm run formatnpm run format:checknpm run check:biomenpm run check:biome:fixnpm run docs:lintnpm run docs:buildnpm run docs:previewnpm run docs:dev:installnpm run docs:dev:startnpm run docs:dev:buildnpm run docs:dev:servenpm run docker:prod:upnpm run docker:prod:down
docs:preview starts a small Fastify server with Scalar at /reference.
docs:build generates a static Scalar reference into docs/reference/.
docs:dev:start starts the Docusaurus developer-doc site on port 3001 by default.
Relevant guides:
docs/developer/docs/docs/guides/getting-started.mddocs/guides/auth-access-matrix.mddocs/guides/authentication.mddocs/guides/alerts.mddocs/guides/errors.mddocs/guides/historical-data.mddocs/guides/historical-exports.mddocs/guides/idempotency.mddocs/guides/observability.mddocs/guides/rate-limits.mddocs/guides/realtime.mddocs/guides/webhooks.md
When adding or changing endpoints:
- update the relevant route and service code
- update the matching OpenAPI path and component files
- add or update API and integration coverage for the new behavior
- update the Docusaurus developer docs if the change affects architecture, business rules, module responsibilities, or local workflow
- update or add guides if the behavior affects client integration
- run
npm run check:biome,npm run docs:lint,npm run test:api, and the relevant integration tests
Realtime stream health scans use REALTIME_STREAM_STALE_SECONDS to decide when an
active SSE subscription is stale enough to create an operational alert.
Hyperwood uses Biome for formatting and linting.
Current repository policy:
- 2-space indentation
- single quotes in JavaScript and TypeScript
- no unused imports
- no unused variables
Useful commands:
npm run lintnpm run lint:fixnpm run formatnpm run format:checknpm run check:biomenpm run check:biome:fix
The test suite is split by intent:
tests/unit/: pure logic with no infrastructure dependencytests/api/: Fastifyinject()tests against the real app instancetests/integration/: PostgreSQL-backed service tests using Testcontainers and real Drizzle migrationstests/helpers/: small reusable bootstrapping helperstests/fixtures/: deterministic payload builders and test data shapestests/setup/: shared Vitest setup and DB lifecycle hooks
Useful commands:
npm testnpm run test:unitnpm run test:apinpm run test:integrationnpm run test:coveragenpm run test:openapi
Notes:
npm testruns the fast unit and API layers.npm run test:integrationrequires Docker because it starts PostgreSQL with Testcontainers.- DB-backed tests apply the real migrations from
drizzle/migrationsand reset state between tests. - The OpenAPI spec remains under
docs/openapi/and is validated withnpm run docs:lintornpm run test:openapi.
npm run devnpm run buildnpm run lintnpm run lint:fixnpm run formatnpm run format:checknpm run check:biomenpm run checknpm testnpm run test:unitnpm run test:apinpm run test:integrationnpm run test:coveragenpm run test:openapinpm run docs:dev:startnpm run docs:dev:installnpm run docs:dev:buildnpm run docs:dev:servenpm run docker:prod:upnpm run docker:prod:downnpm run db:generatenpm run db:migratenpm run docker:local:upnpm run docker:local:down
The first identity migration already exists under drizzle/migrations/ and creates:
usersuser_identitiesuser_sessionsuser_mfa_factorsapi_keys
GET /healthPOST /api/v1/auth/registerPOST /api/v1/auth/loginPOST /api/v1/auth/request-email-verificationPOST /api/v1/auth/verify-emailPOST /api/v1/auth/mfa/totp/verifyPOST /api/v1/auth/mfa/totp/authorizeGET /api/v1/auth/meGET /api/v1/auth/sessionsDELETE /api/v1/auth/sessions/currentDELETE /api/v1/auth/sessions/:sessionIdGET /api/v1/compliance/me/capabilitiesGET /api/v1/funding/methodsGET /api/v1/exchange/scheduleGET /api/v1/exchange/statusGET /api/v1/exchange/feesGET /api/v1/funding/depositsPOST /api/v1/funding/depositsGET /api/v1/funding/withdrawalsPOST /api/v1/funding/withdrawalsPOST /api/v1/webhooks/funding/providers/:providerGET /api/v1/wallet/balanceGET /api/v1/marketsGET /api/v1/markets/:marketIdGET /api/v1/markets/:marketId/streamGET /api/v1/markets/:marketId/order-bookGET /api/v1/markets/:marketId/order-book/deltasGET /api/v1/markets/:marketId/tradesGET /api/v1/historical/markets/:marketId/tradesGET /api/v1/historical/markets/:marketId/candlesGET /api/v1/markets/:marketId/announcementsGET /api/v1/portfolioGET /api/v1/portfolio/streamGET /api/v1/portfolio/fillsGET /api/v1/historical/portfolio/ordersGET /api/v1/historical/portfolio/fillsGET /api/v1/portfolio/settlementsPOST /api/v1/portfolio/exportsGET /api/v1/portfolio/exportsGET /api/v1/portfolio/exports/:exportJobIdPOST /api/v1/ordersPATCH /api/v1/orders/:orderIdDELETE /api/v1/orders/:orderIdPOST /api/v1/auth/api-keysGET /api/v1/auth/api-keysGET /api/v1/auth/api-key/meGET /api/v1/auth/api-key/hmac/meDELETE /api/v1/auth/api-keys/:apiKeyIdPOST /api/v1/auth/api-keys/:apiKeyId/rotatePOST /api/v1/auth/mfa/totp/setupPOST /api/v1/auth/mfa/totp/confirmPOST /api/v1/internal/auth/link-existing-userPOST /api/v1/internal/compliance/users/:userId/profilePOST /api/v1/internal/compliance/users/:userId/restrictionsPOST /api/v1/internal/funding/users/:userId/methodsPOST /api/v1/internal/funding/users/:userId/wallet/seedPOST /api/v1/internal/funding/deposits/:depositId/settlePOST /api/v1/internal/funding/withdrawals/:withdrawalId/approvePOST /api/v1/internal/funding/withdrawals/:withdrawalId/failPOST /api/v1/internal/funding/withdrawals/:withdrawalId/settlePOST /api/v1/internal/funding/reconciliation/runsGET /api/v1/internal/funding/reconciliation/discrepanciesPOST /api/v1/internal/funding/webhook-delay-scanPOST /api/v1/internal/exchange/schedulePOST /api/v1/internal/exchange/feesGET /api/v1/internal/operations/reviewsGET /api/v1/internal/operations/audit-eventsGET /api/v1/internal/operations/rate-limit-eventsGET /api/v1/internal/operations/alertsPOST /api/v1/internal/operations/ledger-invariant-scanPOST /api/v1/internal/operations/settlement-failure-scanPOST /api/v1/internal/operations/settlement-retries/:marketIdPOST /api/v1/internal/operations/trading-condition-scanPOST /api/v1/internal/operations/realtime-stream-health-scanPOST /api/v1/internal/markets/eventsPOST /api/v1/internal/marketsPOST /api/v1/internal/markets/:marketId/matchPOST /api/v1/internal/markets/:marketId/announcementsPOST /api/v1/internal/markets/:marketId/statusPOST /api/v1/internal/markets/:marketId/resolvePOST /api/v1/internal/markets/:marketId/settle
For users with active MFA, sensitive account actions like API key creation and revocation require a short-lived step-up authorization from POST /api/v1/auth/mfa/totp/authorize.
For step-up MFA requests, send:
Authorization: Bearer <session-token>- JSON body with
actionand the 6-digit TOTPcode
For MFA-gated sensitive routes, send:
x-mfa-authorization: short-lived token returned by the authorize route
For HMAC requests, send:
x-api-key: API key prefixx-api-timestamp: unix timestamp in secondsx-api-nonce: unique client noncex-api-signature: hex HMAC-SHA256 ofMETHOD + "\\n" + PATH + "\\n" + TIMESTAMP + "\\n" + NONCE
For order creation requests, send:
Authorization: Bearer <session-token>idempotency-key: stable client-generated key for retried submissions
Funding method discovery and wallet or portfolio reads accept an optional currency query, for example GET /api/v1/funding/methods?currency=BRL.
Funding provider callbacks use:
x-webhook-timestampx-webhook-signature
and are verified with FUNDING_PROVIDER_WEBHOOK_SECRET.
The internal link route requires the x-bootstrap-token header matching INTERNAL_BOOTSTRAP_TOKEN.
The internal compliance routes use the same x-bootstrap-token header and let you upsert KYC/jurisdiction state or place an account under a compliance hold.