Language-agnostic contract for Drawbridge — the public-facing API gateway that fronts the Gatekeeper, the Lightning mediator, the Herald name service, and the DIDComm relay. It enforces an L402 (“Lightning HTTP 402”) paywall and a subscription-credential alternative on every protected route, then proxies the request upstream.
The canonical implementation is services/drawbridge/server/.
Related specs. Drawbridge is a thin proxy. The behaviour of every protected endpoint is the same as the corresponding Gatekeeper / Lightning-mediator / Herald endpoint — read those specs for the downstream contract. This document covers only what Drawbridge adds on top: the L402 challenge/response, subscription auth, per-operation pricing, rate limiting, and the route prefix layout.
The Drawbridge sits on the public network edge of an Archon node. It has three jobs:
X-Subscription-DID header
proving they hold a valid subscription credential issued by this
deployment’s owner; no payment per request./api/v1/did, /dids, /ipfs/*, /block/*, /search, /query)
are forwarded to the local Gatekeeper. Lightning routes are
forwarded to the local Lightning mediator. Herald routes
(/.well-known/*, /names/*) are forwarded to the local Herald. The
DIDComm relay is fronted at /didcomm/* (not paywalled — DIDComm has
its own signed-challenge auth)./ready, /version, /status, /capabilities,
/didcomm-endpoint, /invoice/:did, the Herald well-known endpoints.DIDComm, Lightning, and Herald name resolution are optional — a node
may not deploy them. Each is “offered” iff its downstream URL is
configured (see §8.2); an operator opts a
service out by setting its URL empty (e.g. ARCHON_DIDCOMM_URL=).
When a service is off:
GET /api/v1/capabilities reports it false, and/didcomm, /api/v1/lightning, /invoice/:did,
/names) returns HTTP 501 { "error": "<service> is not enabled
on this node" } rather than proxying to an absent upstream.Clients (e.g. keymaster) read the manifest once and fail fast with a clear message instead of discovering absence via a transport error.
It carries no key material. The macaroon root secret
(ARCHON_DRAWBRIDGE_MACAROON_SECRET) is the only sensitive material on
disk; it MUST be ≥ 32 characters and is used only to sign + verify the
service’s own macaroons (HMAC-SHA-256 internal to macaroons.js).
Binds to ${ARCHON_BIND_ADDRESS}:${ARCHON_DRAWBRIDGE_PORT} (default
0.0.0.0:4222).
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/ready |
Returns <bool> — proxies upstream Gatekeeper readiness. false on connect failure. |
GET |
/api/v1/version |
{ version, commit } (commit is GIT_COMMIT sliced to 7 chars; the sentinel string "unknown" is returned when GIT_COMMIT is unset). |
GET |
/api/v1/capabilities |
{ didcomm, lightning, names } — which optional services this node offers (each true iff its downstream URL is configured). Reflects node intent (config), not live health. See Optional services. |
GET |
/api/v1/status |
{ service: "drawbridge", upstream: GatekeeperStatus, uptime: <seconds>, memoryUsage: <node memUsage> }. HTTP 502 if Gatekeeper is unreachable. |
GET |
/api/v1/didcomm-endpoint |
{ endpoint: <string\|null> } — this node’s public DIDComm relay endpoint for publish-didcomm auto-discovery: <ARCHON_DRAWBRIDGE_PUBLIC_HOST>/didcomm, else the Tor onion only when a SOCKS connection to its hidden service succeeds, else null. |
POST |
/api/v1/l402/pay |
Final step of L402 flow. Body: { paymentHash }. Marks the matching macaroon as paid + returns the bearer credential. See §4.4. |
GET |
/invoice/:did |
Forwarded to lightning-mediator’s /invoice/:did. Returns { paymentRequest, paymentHash, ... }. Used by external zappers to pay any DID that has published a Lightning service. 501 if Lightning is disabled. |
* |
/didcomm/** |
Forwarded to the local DIDComm relay (path prefix /didcomm stripped). Carries the relay’s own routes (mailbox messages/*, signed-challenge, egress deliver). Not paywalled — DIDComm authenticates with its own signed challenge. 501 if DIDComm is disabled. application/didcomm-encrypted+json bodies are captured as text and forwarded verbatim. |
GET |
/.well-known/* |
Forwarded to Herald (e.g. lnurlp/<name>, webfinger, names). |
GET\|POST\|PUT\|DELETE |
/names/* |
Forwarded to Herald (/api/*). 501 if name resolution is disabled. |
GET |
/explorer/** |
Forwarded to the node’s own DID explorer. 501 if the explorer is disabled. Unlike the routes above, the /explorer prefix is not stripped: the explorer’s bundle is built with base=/explorer/ and the app is mounted there, so the prefix has to survive the hop or its asset URLs point somewhere the upstream no longer answers. |
* |
/1.0/identifiers/** |
Forwarded verbatim to the Gatekeeper’s standards-conformant DID resolution / dereferencing surface (/1.0/identifiers/:did, /:did/data, /:did/registration; see Gatekeeper spec §2.6). Not paywalled — public DID resolution for interop with universal resolvers, which do not speak L402. Proxied unchanged (no prefix stripped), so the Gatekeeper’s resolution triple, raw dereferenced resources, and status/error shapes pass through intact. |
GET |
/metrics |
Prometheus exposition. |
When using the bundled Tor onion, Drawbridge probes a SOCKS connection to
its published hidden service every 60 seconds. The
drawbridge_public_onion_reachable gauge is 1 when reachable, 0 when the
hostname file exists but the onion cannot be reached, and -1 when no hostname
is published (or an explicit public host is configured). Changes between these
states are logged. A 0 means DIDComm and Lightning endpoints already present
in DID documents may be unreachable; restarting Tor with its existing keys
restores the same onion. Check that tor is in COMPOSE_PROFILES and the Tor
container is running. DIDComm endpoint discovery reuses a result for at most
30 seconds so public requests cannot trigger an unbounded number of Tor circuits.
The probe has a five-second timeout, and the periodic check keeps the metric
current between discovery requests. It does not alter
/ready or the config-only /capabilities response.
The proxy routes above are unauthenticated by design — a universal resolver does not speak L402, a DIDComm sender must reach a stranger’s mailbox, and a name claim proves DID control rather than presenting a macaroon. The paid path’s limiter keys on the macaroon’s DID and so cannot apply to any of them, which leaves the upstream’s own limits as the only bound. Herald has none.
Each surface therefore carries a bucket of its own, on the same middleware
/didcomm uses:
| Routes | Setting | Default | Budget |
|---|---|---|---|
/didcomm/** |
see DIDComm spec §5.2.1 | requests + bytes | |
/.well-known/*, GET /names/*, /1.0/identifiers/**, /invoice/:did |
ARCHON_DRAWBRIDGE_PUBLIC_READ_PER_SOURCE |
120 | requests, per source |
ARCHON_DRAWBRIDGE_PUBLIC_READ_GLOBAL |
1200 | requests, whole bucket | |
/explorer/** |
ARCHON_DRAWBRIDGE_EXPLORER_READ_PER_SOURCE |
600 | requests, per source |
ARCHON_DRAWBRIDGE_EXPLORER_READ_GLOBAL |
6000 | requests, whole bucket | |
POST\|PUT\|DELETE /names/* |
ARCHON_DRAWBRIDGE_NAME_WRITE_PER_SOURCE |
10 | requests, per source |
ARCHON_DRAWBRIDGE_NAME_WRITE_GLOBAL |
60 | requests, whole bucket | |
| all of the above | ARCHON_DRAWBRIDGE_PUBLIC_RATE_LIMIT_WINDOW |
60s | window |
The explorer is separate because it is chatty. One page load is dozens of asset requests; sharing a budget with DID resolution would let a single visitor exhaust it.
Herald’s writes are separate because they are expensive. PUT /names/api/name
makes Herald issue a verifiable credential and takes a name out of a global
namespace that never refills. Control of a DID is free to obtain, so the write
budget is deliberately small — 10 claims a minute clears any human.
Per-source keying applies only when the source identifies a client, exactly
as on /didcomm: trust proxy is not configured, so behind the bundled Tor
container or any reverse proxy every request arrives from one private address
and only the global bucket binds. See DIDComm spec §5.2.1
for why keying on it anyway would be worse than not keying at all.
A request refused by its per-source bucket is not charged to the global one: it never reaches the upstream, and charging it anyway would let a single source drain the shared budget while already being refused.
Exceeding a budget returns 429 with Retry-After and { error, resetAt }.
If the limiter’s own store is unreachable the check fails open — it protects
availability and must not become the outage it prevents.
| Method | Path | Notes |
|---|---|---|
GET |
/api/v1/l402/status |
Returns service-level L402 status: { enabled, lightning, pricing: [<operationKey>, ...] }. |
POST |
/api/v1/l402/revoke |
Body: { macaroonId: <macaroonId> }. Revokes the macaroon (subsequent verifications return invalid). |
GET |
/api/v1/l402/payments/:did |
List of PaymentRecord for the DID. |
All admin routes require the X-Archon-Admin-Key header. When the
server ARCHON_ADMIN_API_KEY is unconfigured (empty), admin routes
return HTTP 403 { "error": "Admin API key not configured" }. When
configured, a missing or mismatched client header returns HTTP 401.
Constant-time comparison is used.
When L402 is enabled, protected routes require a matching admin key, a valid
L402 bearer, or the explicitly enabled subscription stub. Public read exceptions
are listed in §3.2. On 402 challenge, the response is HTTP 402 with a
WWW-Authenticate: L402 macaroon="<base64>", invoice="<bolt11>" header
and a JSON body with the same fields.
Each route is a 1:1 forward to the upstream Gatekeeper after auth passes; the response shape is identical to the Gatekeeper’s. See the Gatekeeper spec §2 for each route’s contract.
| Method | Path |
|---|---|
GET |
/api/v1/registries |
POST |
/api/v1/did |
POST |
/api/v1/did/generate |
GET |
/api/v1/did/:did (forwards versionTime/versionSequence/confirm/verify query params) |
GET |
/api/v1/did/:did/genesis (immutable genesis DID document; no publisher authorization claim) |
POST |
/api/v1/dids |
POST |
/api/v1/dids/export |
POST/GET |
/api/v1/ipfs/json[/:cid] |
POST/GET |
/api/v1/ipfs/text[/:cid] |
POST/GET |
/api/v1/ipfs/data[/:cid] |
POST/GET |
/api/v1/ipfs/stream[/:cid] (true streaming both directions; no body buffering) |
GET |
/api/v1/block/:registry/latest |
GET |
/api/v1/block/:registry/:blockId |
GET |
/api/v1/search?q= |
POST |
/api/v1/query |
Plus a wildcard mount of all Lightning routes, forwarded to the Lightning mediator:
| Method | Path |
|---|---|
* |
/api/v1/lightning/** |
The path tail is appended onto the upstream URL untouched. See the Lightning mediator spec.
Global Express limits (json, urlencoded, text, raw for
application/octet-stream): 10 MB each. A dedicated text parser
for application/didcomm-encrypted+json (also 10 MB) captures DIDComm
envelopes so they survive proxying to the relay byte-for-byte. The IPFS
stream POST (/api/v1/ipfs/stream) explicitly bypasses the raw parser
so the upstream Kubo client can stream the request body directly.
cors() with permissive defaults (any origin, no
credentials)./.well-known/*, /names/*): cors({ origin:
true, credentials: true }) — required for browser flows that depend
on session cookies (Herald login, OAuth/OIDC).All error responses are application/json { "error": "<message>" }
unless a downstream proxy returned its own body verbatim. Standard
status codes:
401 — auth header malformed or admin key wrong402 — L402 challenge (with WWW-Authenticate header)403 — admin not configured / scope insufficient429 — rate-limited501 — the requested optional service (DIDComm / Lightning / names) is not enabled on this node502 — upstream Gatekeeper / Lightning / Herald / DIDComm unreachable503 — Lightning service unavailable (L402 invoice creation failed)The route label is the request path with dynamic segments collapsed:
/did/<did> -> /did/:did
/invoice/<did> -> /invoice/:did
/ipfs/json/<cid> -> /ipfs/json/:cid
/ipfs/text/<cid> -> /ipfs/text/:cid
/ipfs/data/<cid> -> /ipfs/data/:cid
/ipfs/stream/<cid> -> /ipfs/stream/:cid
/queue/<registry> -> /queue/:registry
/block/<registry>/latest -> /block/:registry/latest
/block/<registry>/<id> -> /block/:registry/:blockId
/payments/<did> -> /payments/:did
The label does NOT include the /api/v1 prefix.
Protected routes first accept a nonempty X-Archon-Admin-Key matching
Drawbridge’s ARCHON_ADMIN_API_KEY, using the same constant-time comparison
as its admin routes. This exemption grants no new administrative capability:
holders of this key already administer the paywall. Exempt requests create no
invoice and consume no paid quota. They increment
drawbridge_l402_admin_bypasses_total; public bypass routes do not.
Both Keymaster service runtimes can send an explicitly configured upstream credential. To combine server-side DIDComm and L402 with the bundled Compose:
ARCHON_KEYMASTER_GATEKEEPER_URL=http://drawbridge:4222
ARCHON_KEYMASTER_GATEKEEPER_API_KEY=${ARCHON_ADMIN_API_KEY}
Compose maps the latter to ARCHON_GATEKEEPER_API_KEY inside Keymaster. Outside
Compose, set ARCHON_GATEKEEPER_URL and ARCHON_GATEKEEPER_API_KEY directly.
The upstream key defaults to empty; Keymaster never automatically forwards its
own admin key to a potentially remote upstream. Configure this credential only
for a trusted endpoint. Browser wallets do not receive the exemption automatically.
An empty server key never enables exemption. A missing or incorrect header continues through the ordinary payment flow, including accepting a valid L402 bearer; it does not cause an admin-auth rejection on proxy routes.
For other requests, the L402 middleware looks for an Authorization: L402 <macaroon>:<preimage>
header. If valid, the request proceeds. Otherwise it issues a 402
challenge.
If ARCHON_DRAWBRIDGE_L402_ENABLED=false, the auth middleware chain
is empty and all proxy routes are open. This is intentional for
private / single-tenant deployments.
Setting ARCHON_DRAWBRIDGE_SUBSCRIPTIONS_ENABLED=true prepends a
subscription-auth middleware ahead of L402. The current middleware is
a stub and performs no verification whatsoever: any request carrying
an X-Subscription-DID header — any value, no credential resolution, no
signature, no expiry or revocation check — is marked
subscription-authenticated, and the L402 middleware then skips payment
verification for it. Enabling this flag therefore lets any caller bypass
the paywall with a single header. It exists only so the dual-auth flow
can be exercised before the subscription system is built.
Real credential verification is tracked in #121. Until that lands, leave
this flag false in any deployment that is not a throwaway dev instance.
The L402 middleware (mounted on the /api/v1 router) has a hard-coded
bypass list for paths that should never be paywalled. Paths are matched
against the request path with the /api/v1 prefix stripped:
/ready, /version, /status, /metrics, /capabilities, /didcomm-endpoint/l402//did/ or /ipfs/(Auth is also applied per-route — only the protected proxy routes spread the auth middleware into their handler chain — so the public routes above carry no auth regardless; the bypass list is belt-and-suspenders.)
All other routes are handled outside this router and so are not subject
to the L402 middleware at all (e.g. /invoice/:did, /didcomm/*,
/.well-known/*, /names/*).
Implementations MUST honor this list — clients depend on at least
/ready, /version, and the GET DID/IPFS read paths being free.
The L402 protocol is a Lightning-paid HTTP authentication scheme. The Drawbridge implements a slight variant compatible with macaroons.js:
When auth fails on a protected route, Drawbridge:
resolveDID, getDIDs) by
matching the route to the pricing config (see §5).ARCHON_DRAWBRIDGE_DEFAULT_PRICE_SATS (default 10).POST /api/v1/l402/invoice to create
a CLN invoice for that amount.did = <header X-DID or empty>scope = <operation key>expiry = <now + ARCHON_DRAWBRIDGE_INVOICE_EXPIRY seconds> (default 3600)payment_hash = <invoice payment hash>POST /api/v1/l402/pending
on the Lightning mediator (the mediator owns the key-value store
for invoice → macaroon lookup).WWW-Authenticate: L402 macaroon="<base64>", invoice="<bolt11>"{ macaroon, invoice } for clients that don’t parse
the header.Macaroons are built with macaroons.js
and serialized with the library’s default (binary, base64-wrapped).
Caveats are encoded as plain text strings of the form <key> = <value>:
did = did:cid:bagaaiera...
scope = resolveDID
expiry = 1726700000
max_uses = 100
payment_hash = abc123...
Implementations SHOULD use a macaroons.js-compatible library (macaroons.py, libmacaroons, macaroon-rs, etc.). A minimal custom implementation MUST:
rootSecret = ARCHON_DRAWBRIDGE_MACAROON_SECRET
as the keyed-MAC primitive.http://localhost:<port> (yes, even on
remote servers — the location is informational, not a routing hint).Clients pay the invoice, retrieve the preimage from their Lightning wallet, and resubmit:
Authorization: L402 <base64-macaroon>:<hex-preimage>
Drawbridge’s L402 middleware:
:.sha256(preimage) == payment_hash from the macaroon
caveats.rootSecret.did matches the request’s X-DID (or empty).scope matches the operation key for the requested route.expiry > now.currentUses < maxUses (incremented on every accepted request).payment_hash matches the now-verified preimage.finish event, if
the upstream status was < 500, increments currentUses in the
store.On any failure: HTTP 401 with a fresh 402 challenge.
POST /api/v1/l402/pay exists for a polling flow where the client wants
Drawbridge to confirm the payment server-side rather than presenting
the credential immediately:
{ paymentHash }. (Any preimage in the body is IGNORED.)GET /api/v1/l402/pending/:paymentHash)./api/v1/l402/check) and verifies it matches the payment hash.{ macaroonId, macaroon, paymentHash, method, amountSat, preimage }.Per-operation prices are loaded at startup. Only two individual operations have dedicated environment-variable overrides:
ARCHON_DRAWBRIDGE_PRICE_CREATE_DID — sats for createDID
(accepts >= 0).ARCHON_DRAWBRIDGE_PRICE_RESOLVE_DID — sats for resolveDID
(must be > 0).For any other operation, set the JSON override
ARCHON_DRAWBRIDGE_PRICING (see §8.2).
The full set of operation keys (used by the route-to-scope mapper):
| Key | Routes |
|---|---|
resolveDID |
GET /did/:did |
createDID |
POST /did |
generateDID |
POST /did/generate |
getDIDs |
POST /dids |
exportDIDs |
POST /dids/export |
importDIDs |
POST /dids/import |
removeDIDs |
POST /dids/remove |
exportBatch |
POST /batch/export |
importBatch |
POST /batch/import |
importBatchByCids |
POST /batch/import/cids |
getQueue |
GET /queue/:registry |
clearQueue |
POST /queue/:registry/clear |
processEvents |
POST /events/process |
listRegistries |
GET /registries |
searchDIDs |
GET /search |
queryDIDs |
POST /query |
getBlock |
GET /block/:registry/:blockId and /latest |
addBlock |
POST /block/:registry |
addJSON |
POST /ipfs/json |
getJSON |
GET /ipfs/json/:cid |
addText |
POST /ipfs/text |
getText |
GET /ipfs/text/:cid |
addData |
POST /ipfs/data |
getData |
GET /ipfs/data/:cid |
The IPFS streaming routes (/ipfs/stream, /ipfs/stream/:cid) have no
explicit scope entry and so fall through to the default price.
Any operation without an explicit price uses
ARCHON_DRAWBRIDGE_DEFAULT_PRICE_SATS. createDID accepts a value of
0 (free); resolveDID requires > 0. Other free operations must be
configured via the ARCHON_DRAWBRIDGE_PRICING JSON override or by
disabling L402 entirely.
Per-DID sliding window stored in Redis. Request ledger key:
drawbridge:ratelimit:<did> (a sorted set of per-request members
scored by timestamp, TTL = rateLimitWindow seconds). Each
authenticated request adds an entry; if more than rateLimitMax
entries fall in the last rateLimitWindow seconds, the request is
rejected with HTTP
429 { "error": "Rate limit exceeded", "resetAt": <unix> }.
Defaults: 100 requests / 60 seconds. Configured via:
ARCHON_DRAWBRIDGE_RATE_LIMIT_MAXARCHON_DRAWBRIDGE_RATE_LIMIT_WINDOWAnonymous (unauthenticated) requests are rate-limited per-IP. The DID
used for rate-limiting is read from the X-DID request header (the L402
middleware also uses this header for the macaroon’s did caveat).
Namespace: drawbridge:. All values are JSON strings unless noted.
| Key | Type | TTL | Contents |
|---|---|---|---|
drawbridge:macaroon:<id> |
STRING | none | MacaroonRecord |
drawbridge:payment:<id> |
STRING | none | PaymentRecord |
drawbridge:payments:did:<did> |
SORTED SET | none | Payment IDs for that DID, scored by createdAt |
drawbridge:ratelimit:<did> |
SORTED SET | windowSeconds |
Sliding-window members scored by request timestamp |
MacaroonRecord:
{
"id": "<hex>",
"did": "<DID or empty>",
"scope": ["resolveDID", ...],
"createdAt": <unix ms>,
"expiresAt": <unix ms>,
"maxUses": <int>,
"currentUses": <int>,
"paymentHash": "<hex>",
"revoked": <bool>
}
PaymentRecord — same shape as in the
Lightning mediator spec §5.
A new implementation MUST use this schema if the Redis instance is shared with the reference TypeScript service.
ARCHON_DRAWBRIDGE_MACAROON_SECRET is ≥ 32
chars — exit 1 on failure.waitUntilReady=true, retry-with-backoff).
Exit on persistent failure.| Variable | Default | Meaning |
|---|---|---|
ARCHON_DRAWBRIDGE_PORT |
4222 |
HTTP listen port. |
ARCHON_BIND_ADDRESS |
0.0.0.0 |
|
ARCHON_GATEKEEPER_URL |
http://localhost:4224 |
Upstream Gatekeeper. |
ARCHON_HERALD_URL |
http://localhost:4230 |
Upstream Herald (for /.well-known/* and /names/*). Set empty to disable name resolution (capability names:false, /names → 501). |
ARCHON_LIGHTNING_MEDIATOR_URL |
http://localhost:4235 |
Upstream for Lightning routes + L402 invoice/pending storage. Set empty to disable Lightning (capability lightning:false, /lightning + /invoice/:did → 501). |
ARCHON_EXPLORER_URL |
http://localhost:4000 |
Upstream DID explorer proxied at /explorer. Set empty to disable it (capability explorer:false, /explorer → 501). Belongs to the explorer profile — blank it when that profile is off, or Herald links visitors at a container that is not running. |
ARCHON_DIDCOMM_URL |
http://localhost:4236 |
Upstream DIDComm relay proxied at /didcomm. Set empty to disable DIDComm (capability didcomm:false, /didcomm → 501). The bundled compose defaults this empty (DIDComm is opt-in, off by default); enabling requires the didcomm profile and setting this URL. |
ARCHON_DRAWBRIDGE_PUBLIC_HOST |
empty | Public base URL this node is reachable at (clearnet host or Tor onion). Used by /didcomm-endpoint to advertise <host>/didcomm. |
ARCHON_DRAWBRIDGE_TOR_HOSTNAME_FILE |
/data/tor/hostname |
When PUBLIC_HOST is empty, /didcomm-endpoint falls back to the Tor onion read from this shared hidden-service hostname file. |
ARCHON_DRAWBRIDGE_TOR_PROXY |
tor:9050 |
SOCKS5 proxy used to check that the published hidden service is reachable. Set to the Docker network address for bundled Tor. |
ARCHON_REDIS_URL |
redis://localhost:6379 |
Macaroon/payment/rate-limit store. |
ARCHON_ADMIN_API_KEY |
empty | Required for L402 admin routes. Empty → admin routes 403. |
ARCHON_DRAWBRIDGE_L402_ENABLED |
false |
When false, all proxy routes open. |
ARCHON_DRAWBRIDGE_SUBSCRIPTIONS_ENABLED |
false |
Insecure dev stub (#121). When true, any request carrying an X-Subscription-DID header bypasses L402 payment verification with no credential check. See §3.1. |
ARCHON_DRAWBRIDGE_MACAROON_SECRET |
empty (required) | HMAC root secret. ≥ 32 chars. |
ARCHON_DRAWBRIDGE_DEFAULT_PRICE_SATS |
10 |
Per-request price for unmapped operations. |
ARCHON_DRAWBRIDGE_INVOICE_EXPIRY |
3600 |
Macaroon expiry, seconds. |
ARCHON_DRAWBRIDGE_RATE_LIMIT_MAX |
100 |
|
ARCHON_DRAWBRIDGE_RATE_LIMIT_WINDOW |
60 |
Seconds. |
ARCHON_DRAWBRIDGE_PRICE_CREATE_DID |
unset | Sats price for createDID (accepts >= 0). |
ARCHON_DRAWBRIDGE_PRICE_RESOLVE_DID |
unset | Sats price for resolveDID (must be > 0). |
ARCHON_DRAWBRIDGE_PRICING |
unset | JSON pricing override; { "operations": { "<scope>": { "amountSat": <n>, "description": "..." } } }. Merged on top of the per-operation env vars. |
GIT_COMMIT |
unknown |
Build commit. |
Off-switch semantics. The optional-service URLs (
ARCHON_DIDCOMM_URL,ARCHON_LIGHTNING_MEDIATOR_URL,ARCHON_HERALD_URL) are read with??, not||: an unset var falls back to the default (service on), while an explicitly empty value disables the service. This is what drives/capabilitiesand the 501 gating.
SIGTERM/SIGINT → close HTTP listener → disconnect Redis → exit 0.
| Metric | Type | Labels |
|---|---|---|
drawbridge_http_requests_total |
counter | method, route, status |
drawbridge_http_request_duration_seconds |
histogram (buckets: 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5) |
method, route |
drawbridge_l402_admin_bypasses_total |
counter | none |
drawbridge_l402_challenges_total |
counter | did_known ("true"/"false") |
drawbridge_l402_verifications_total |
counter | result ("success"/"failure") |
drawbridge_version_info |
gauge | version, commit |
Plus the standard Prometheus process metrics. Route normalization rules in §2.7.
pino at LOG_LEVEL (default info); HTTP via pino-http in
production or morgan('dev') otherwise. Errors logged as structured
{ err } objects with the upstream URL when a proxy call fails.
No log lines are contractual for downstream consumers.
ghcr.io/archetech/drawbridgeUnit and router tests live in tests/drawbridge/, including internal admin-key
exemption, payment fallback, and admin-route rejection. Additional validation:
A conformant third implementation MUST:
GET /api/v1/capabilities and apply the optional-service
off-switch + 501 gating (§1 Optional services):
a service whose downstream URL is empty reports false and its proxy
mount returns 501.WWW-Authenticate: L402 macaroon="...", invoice="...", JSON body
with the same fields.sha256(preimage) == payment_hash constant-time.ARCHON_ADMIN_API_KEY is configured (so the upstream services accept
the proxy request).