Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 60 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ npm (or yarn/pnpm)
Setup
Bash

# Clone the repo (or use your fork)
Clone the repo (or use your fork)
git clone <repo-url>
cd anchornet-backend

# Install dependencies
Install dependencies
npm install

# Run in development
Run in development
npm run dev
Server runs at http://localhost:3001 by default. Set PORT to override.

Scripts
Command Description
npm run dev Start dev server with hot reload
npm run build Compile TypeScript to dist/
npm start Run production build
npm test Run tests (Jest)
npm run lint Run ESLint
Command Description
npm run dev Start dev server with hot reload
npm run build Compile TypeScript to dist/
npm start Run production build
npm test Run tests (Jest)
npm run lint Run ESLint
API
Service
GET /health – health check
Expand Down Expand Up @@ -67,7 +67,15 @@ POST /api/v1/anchors – register an anchor { id, name? } (409 if it exists)
POST /api/v1/anchors/bulk – register a batch of anchors atomically
{ anchors: [{ id, name? }, ...] }; validates and checks every entry (against
both the existing registry and duplicates within the batch) before storing
any of them, so one bad entry never leaves a partial batch registered
any of them, so one bad entry never leaves a partial batch registered.
Supports ?dryRun=true for a read-only preflight check: the batch runs
through the identical validation and returns the same success/error
outcome (201 with the would-be-registered anchors, or the same 400/409),
but nothing is persisted. Successful responses include a dryRun boolean
so callers can confirm whether the batch was committed. The flag is
strictly parsed — only "true" or "false" (any casing) is accepted, and
any other value is a 400, so a typo can never silently perform a real
registration
GET /api/v1/anchors – list anchors; supports ?status=active or
?status=inactive (400 for any other value), a free-text ?q= search
over id/name (case-insensitive substring match), ?sort=id|name|registeredAt
Expand Down Expand Up @@ -128,10 +136,10 @@ Initial request: Send a POST request to register an anchor with a unique Idempot

Bash

curl -i -X POST http://localhost:3001/api/v1/anchors \
-H "Content-Type: application/json" \
-H "Idempotency-Key: register-anchor-xyz" \
-d '{"id": "anchor-xyz", "name": "Anchor XYZ"}'
curl -i -X POST http://localhost:3001/api/v1/anchors
-H "Content-Type: application/json"
-H "Idempotency-Key: register-anchor-xyz"
-d '{"id": "anchor-xyz", "name": "Anchor XYZ"}'
Response:

http
Expand All @@ -141,19 +149,19 @@ Content-Type: application/json; charset=utf-8
x-request-id: df743737-896c-4e4f-8dae-1c08a95302cd

{
"id": "anchor-xyz",
"name": "Anchor XYZ",
"registeredAt": "2026-07-22T14:17:57.537Z",
"active": true
"id": "anchor-xyz",
"name": "Anchor XYZ",
"registeredAt": "2026-07-22T14:17:57.537Z",
"active": true
}
Subsequent replay: Send the exact same request again using the same Idempotency-Key. The server returns the cached 201 response immediately, bypassing the normal handler and avoiding a 409 (which would normally happen for duplicate anchor registration):

Bash

curl -i -X POST http://localhost:3001/api/v1/anchors \
-H "Content-Type: application/json" \
-H "Idempotency-Key: register-anchor-xyz" \
-d '{"id": "anchor-xyz", "name": "Anchor XYZ"}'
curl -i -X POST http://localhost:3001/api/v1/anchors
-H "Content-Type: application/json"
-H "Idempotency-Key: register-anchor-xyz"
-d '{"id": "anchor-xyz", "name": "Anchor XYZ"}'
Response (Cached):

http
Expand All @@ -163,19 +171,19 @@ Content-Type: application/json; charset=utf-8
x-request-id: 4a123f52-1623-429b-ba67-3d0d0d5c2eb0

{
"id": "anchor-xyz",
"name": "Anchor XYZ",
"registeredAt": "2026-07-22T14:17:57.537Z",
"active": true
"id": "anchor-xyz",
"name": "Anchor XYZ",
"registeredAt": "2026-07-22T14:17:57.537Z",
"active": true
}
Mismatched body (Known Gap): If you reuse the same Idempotency-Key but change the request payload (e.g., modifying the name field), the server will still return the cached 201 response corresponding to the first payload. Detecting mismatched request bodies (which would ideally return a 422 error) is currently a known gap in this system.

Bash

curl -i -X POST http://localhost:3001/api/v1/anchors \
-H "Content-Type: application/json" \
-H "Idempotency-Key: register-anchor-xyz" \
-d '{"id": "anchor-xyz", "name": "Anchor XYZ Modified Name"}'
curl -i -X POST http://localhost:3001/api/v1/anchors
-H "Content-Type: application/json"
-H "Idempotency-Key: register-anchor-xyz"
-d '{"id": "anchor-xyz", "name": "Anchor XYZ Modified Name"}'
Response (Replayed from the original cached version):

http
Expand All @@ -185,10 +193,10 @@ Content-Type: application/json; charset=utf-8
x-request-id: 184c8357-3fc3-4e2f-a87c-19042ab804fe

{
"id": "anchor-xyz",
"name": "Anchor XYZ",
"registeredAt": "2026-07-22T14:17:57.537Z",
"active": true
"id": "anchor-xyz",
"name": "Anchor XYZ",
"registeredAt": "2026-07-22T14:17:57.537Z",
"active": true
}
The process shuts down gracefully on SIGTERM/SIGINT: it stops accepting
new connections, closes the HTTP server, marks /health/ready unready, and
Expand All @@ -204,27 +212,27 @@ operators can pause writes without taking the whole API down.
Configuration
The application is configured using environment variables. Every environment variable read by config.ts is listed below with its default and valid range/format:

Variable Default Valid Range / Format Description
PORT 3001 Positive integer (typically 1 - 65535) HTTP port the server binds to. Non-numeric values fall back to default.
FEE_BPS 10 Integer between 0 and 10000 (inclusive) Protocol fee in basis points applied to settlements and quotes. The process throws an error and fails to start if configured outside this range.
API_KEY (Unset) Any non-empty string If set, mutating requests (POST/PUT/PATCH/DELETE) must send an matching x-api-key header. Whitespace-only values are treated as unset.
CORS_ORIGIN (Unset) Comma-separated list of origin URLs Allowed CORS origins. Whitespace around entries is trimmed; empty entries are ignored. If unset, every origin is permitted.
BODY_LIMIT 100kb Express bytes-compatible string (e.g., "500kb", "2mb") Maximum accepted JSON request body size. Default is applied if value is blank.
MAINTENANCE_MODE false "1", "true" (case-insensitive) to enable When enabled, mutating requests are rejected with a 503 Service Unavailable error, while read requests continue to function normally.
NODE_ENV development Any environment name string (e.g., "development", "production", "test") Specifies the runtime environment name.
METRICS_SNAPSHOT_INTERVAL_MS (Unset) Positive integer Optional interval in milliseconds to automatically take metrics snapshots.
IDEMPOTENCY_TTL_MS 86400000 (24h) Positive integer Milliseconds that a cached response remains eligible for idempotency replay.
RATE_LIMIT_MAX 30 Positive integer Maximum mutating requests allowed per client within the rolling rate-limiting window.
RATE_LIMIT_WINDOW_MS 60000 (1 min) Positive integer Length of the rolling rate-limiting window, in milliseconds.
Variable Default Valid Range / Format Description
PORT 3001 Positive integer (typically 1 - 65535) HTTP port the server binds to. Non-numeric values fall back to default.
FEE_BPS 10 Integer between 0 and 10000 (inclusive) Protocol fee in basis points applied to settlements and quotes. The process throws an error and fails to start if configured outside this range.
API_KEY (Unset) Any non-empty string If set, mutating requests (POST/PUT/PATCH/DELETE) must send an matching x-api-key header. Whitespace-only values are treated as unset.
CORS_ORIGIN (Unset) Comma-separated list of origin URLs Allowed CORS origins. Whitespace around entries is trimmed; empty entries are ignored. If unset, every origin is permitted.
BODY_LIMIT 100kb Express bytes-compatible string (e.g., "500kb", "2mb") Maximum accepted JSON request body size. Default is applied if value is blank.
MAINTENANCE_MODE false "1", "true" (case-insensitive) to enable When enabled, mutating requests are rejected with a 503 Service Unavailable error, while read requests continue to function normally.
NODE_ENV development Any environment name string (e.g., "development", "production", "test") Specifies the runtime environment name.
METRICS_SNAPSHOT_INTERVAL_MS (Unset) Positive integer Optional interval in milliseconds to automatically take metrics snapshots.
IDEMPOTENCY_TTL_MS 86400000 (24h) Positive integer Milliseconds that a cached response remains eligible for idempotency replay.
RATE_LIMIT_MAX 30 Positive integer Maximum mutating requests allowed per client within the rolling rate-limiting window.
RATE_LIMIT_WINDOW_MS 60000 (1 min) Positive integer Length of the rolling rate-limiting window, in milliseconds.
Architecture
text

routes/ HTTP layer (thin controllers)
services/ business rules (liquidity, quotes, anchors, settlements)
repositories/ in-memory stores (swappable for an indexer)
middleware/ request id, logging, API-key auth, rate limiting, error handling
models/ domain types
config.ts env-based configuration
routes/ HTTP layer (thin controllers)
services/ business rules (liquidity, quotes, anchors, settlements)
repositories/ in-memory stores (swappable for an indexer)
middleware/ request id, logging, API-key auth, rate limiting, error handling
models/ domain types
config.ts env-based configuration
Contributing
Fork the repo and create a branch from main.
Install deps: npm install. Run tests: npm test; lint: npm run lint.
Expand Down
13 changes: 13 additions & 0 deletions src/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,17 @@ describe("openapi spec", () => {
res.body.paths["/api/v1/liquidity/{anchor}/{asset}"].delete,
).toBeDefined();
});

it("documents the dryRun preflight parameter on POST /api/v1/anchors/bulk", () => {
const spec = buildOpenApiSpec() as {
paths: Record<
string,
{ post: { parameters?: string[]; description?: string } }
>;
};
const operation = spec.paths["/api/v1/anchors/bulk"].post;

expect(operation.parameters).toEqual(expect.arrayContaining(["dryRun"]));
expect(operation.description).toContain("dryRun=true");
});
});
13 changes: 12 additions & 1 deletion src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,18 @@ export function buildOpenApiSpec(): Record<string, unknown> {
post: { summary: "Reactivate a previously deactivated anchor" },
},
"/api/v1/anchors/bulk": {
post: { summary: "Register a batch of anchors atomically" },
post: {
summary: "Register a batch of anchors atomically",
description:
"Validates every entry (against both the existing registry and " +
"duplicate ids within the batch) before storing any of them. " +
"Pass ?dryRun=true to run that identical validation as a " +
"read-only preflight check: the response reports the same " +
"success/error outcome and the would-be-registered anchors, but " +
'nothing is persisted. `dryRun` accepts only "true" or ' +
'"false"; any other value is a 400.',
parameters: ["dryRun"],
},
},
"/api/v1/anchors/{id}/settlements": {
get: {
Expand Down
Loading
Loading