Configuration
The service reads its entire runtime config from a single YAML file. The path is selected by the CONFIG_PATH environment variable; the bundled docker-compose.yml sets it to /src/config/config.yaml, which corresponds to ./config/config.yaml on the host.
Top-level keys
| Key | Type | Default | Description |
|---|---|---|---|
port | number | required | TCP port the server binds to. |
logLevel | error | warn | info | debug | info | info = morgan HTTP logs; debug = verbose per-request log line incl. IP, XFF, UA. |
bitcoinNetwork | MAINNET | TESTNET | TESTNET3 | TESTNET4 | required | Which Bitcoin network the SDK connects to. TESTNET is an alias for TESTNET3. |
starknetRpc / solanaRpc / botanixRpc / citreaRpc / alpenRpc / goatRpc | string or null | null (disabled) | RPC URL per smart chain. Omit / set to null to disable that chain. |
swapsSyncIntervalSeconds | number | 300 | Interval between background SwapperApi.sync() calls (purges expired swaps, refreshes state). |
reloadLpIntervalSeconds | number | 300 | Interval between background LP reloads (re-discovers dropped LPs). |
cors | object or null | null (disabled) | Passed through to the cors middleware. |
rateLimit | { windowMs, maxRequests } | required | Global fallback rate limit (applied when an auth path does not override). |
auth | array | required, non-empty | Ordered list of auth paths — see Authentication. |
https | { keyPath, certPath } or null | null (HTTP) | TLS config. Paths are resolved relative to the config file. |
trustProxy | boolean | false | When running the API behind a reverse proxy, set to true to properly parse the client's IP address. |
Authentication
auth is an ordered array — the first entry that matches a request wins. Each entry can optionally set its own rateLimit (or null to disable rate limiting entirely on that path).
Three entry types:
auth:
# 1. Shared-secret API key
- type: apiKey
name: "Privileged clients"
apiKey: "replace-with-long-random-secret"
header: x-api-key # optional, default x-api-key
rateLimit: null # null = no rate limit on this path
# 2. JWT signed by an external auth service
- type: jwt
name: "Premium Users"
publicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
algorithms: [RS256] # or [ES256], etc.
claims: # optional — all claims must match
user_tier: "swapper"
rateLimit:
windowMs: 60000
maxRequests: 200
# 3. Open fallback (uses global rateLimit)
- type: none
name: "Public"
API-key auth
Any client that can present the configured shared secret in a header (default x-api-key) is authorized on this path.
GET /listSwaps?signer=0x... HTTP/1.1
x-api-key: replace-with-long-random-secret
Treat the API key as a shared secret. Whether you ship it to a trusted backend, embed it in a first-party frontend, or hand it to an operator depends on your threat model — just never expose it to clients you do not control.
JWT auth
Requests are authorized by a signed JWT in the Authorization: Bearer <jwt> header. The service:
- Verifies the JWT signature against the public key configured in
config.yamlusing the algorithms listed inalgorithms. - Enforces the standard
expclaim. - Checks any additional required claims listed under
claimson the auth entry.
How the JWT is minted and delivered to the caller is out of scope for this service — issue it from any auth system that can sign with a key pair whose public half you paste into config.yaml.
claims supports two forms:
- Exact match:
{ user_tier: "swapper" }— the JWT payload must contain a matching scalar value. - Array-includes:
{ permissions: { includes: "swap_permission" } }— the JWT payload must contain an array that includes the given value.
Generating a test key pair + JWT
A helper script is bundled for local testing:
npx ts-node --project tsconfig.scripts.json \
scripts/generate-jwt.ts \
'{"payload":{"sub":"demo","user_tier":"swapper"},"options":{"expiresIn":"1h"}}'
It prints:
- A freshly generated ES256 (P-256) private key (PEM).
- The matching public key in both multi-line and single-line PEM form — paste the single-line form into
auth[].publicKeyinconfig.yaml. - A signed JWT you can use immediately with
Authorization: Bearer ....
Pass an existing private key as the second argument to sign with your own key instead.
Public / no-auth
type: none matches any request. Put it last if you want to offer anonymous access; omit it if all traffic must be authenticated.
Rate limiting
Uses in-memory bucketing per client IP, with a fixed window.
- Each auth entry can set its own
rateLimit: { windowMs, maxRequests }or explicitlynull(no limit — typical forapiKeytraffic). - If an auth entry has no
rateLimitkey, the globalrateLimitfrom the top level applies. - Exceeding the limit returns
429 { error: "Rate limit exceeded", retryAfter }.
HTTPS and certificate reload
Set https in the config to run TLS directly:
https:
keyPath: "./tls/server.key"
certPath: "./tls/server.cert"
Both paths are resolved relative to the config.yaml file. With the bundled compose layout that means you can keep the certificate, key, or symlinks to them under config/tls/ and mount the whole config/ directory into the container read-only.
The server watches both files with a 1 s poll interval. On any change it schedules a 60 s-delayed reload (debounced) via server.setSecureContext(...) — Node keeps serving existing connections during the swap. This is designed to work cleanly with Let's Encrypt / certbot renewal hooks: the renewal hook writes both files, the server picks them up within a minute without a restart.
Running behind a reverse proxy
Set trustProxy if you run the API behind a reverse proxy and want to correctly resolve client IP addresses (important for rate limiting):
trustProxy: true
You can also let the reverse proxy handle TLS and omit the https section from the API config.
Persistence
The container writes SQLite files into the directory pointed to by the STORAGE_DIR environment variable. The bundled compose file sets it to /src/storage, mapped to ./storage on the host:
CHAIN_atomiqsdk-1-<CHAINID>.sqlite3— one per active smart chain; swap state for that chain.STORE_<name>.sqlite3— additional SDK state (e.g.solAccounts).
The bundled docker-compose.yml uses two bind mounts so swap state and config live as plain files in the project directory:
| Host path | Container path | Mode | Purpose |
|---|---|---|---|
./config | /src/config | read-only | config.yaml and (optionally) tls/ |
./storage | /src/storage | read-write | SQLite swap state |
Back up ./storage/ to preserve in-flight swaps across host migrations.
Background maintenance timers
atomiq-api-docker runs two timers (configurable):
| Timer | Interval key | Default | Purpose |
|---|---|---|---|
| Swap sync | swapsSyncIntervalSeconds | 300 s | Calls SwapperApi.sync(). Refreshes state for active swaps and purges expired swaps from the local DB. |
| LP reload | reloadLpIntervalSeconds | 300 s | Re-discovers Atomiq LPs, so a dropped LP can rejoin the quote pool without restarting the container. |
Errors in either timer are logged and the timer continues.
Error handling
All endpoints return JSON. Errors come in two shapes:
| Status | Body | Meaning |
|---|---|---|
| 400 | { "error": "<message>" } | Validation error or SDK rejected the request. |
| 401 | { "error": "Unauthorized" } | No auth entry matched. |
| 429 | { "error": "Rate limit exceeded", "retryAfter": <seconds> } | Per-IP, per-auth-path or global bucket exhausted. |
Rate-limit state is per IP. If you use a reverse proxy in front set trustProxy: true in the config.
Security notes
- JWT
exp— the JWT auth path enforcesexp, so you can tune expiration of the JWT. - Public key rotation — changing
auth[].publicKeyrequires a restart. Plan a rollover window by temporarily listing both the old and new key as two JWT auth entries. - CORS —
origin: "*"is fine for public endpoints, but in production you should restrict it to your wallet front-end origin(s).