API reference
POST /api/intent is the only L402-gated route (2 sats per call). The other eight are unauthenticated — including POST /api/deposit, which returns a BOLT11 to settle out-of-band. JSON envelope. Machine-readable spec at /openapi.json.Base URL
https://satrank.dev
Self-hosters can run their own deployment ; the MCP package proxies to whatever SATRANK_API_BASE resolves to.
Response envelope
Every JSON response uses one of two shapes:
// Success
{ "data": { ... } }
// Error
{ "error": { "code": "<CODE>", "message": "<optional human-readable>" } }
Some legacy endpoints (/health, /.well-known/satrank-key) return a flat object without the envelope — this is intentional. Everything under /api/* uses the envelope.
Authentication
Two authenticated endpoints, both via L402 :
| Header form | Used for |
|---|---|
| Authorization: L402 <macaroon_b64>:<preimage_hex> | Single-use 2-sat call. Macaroon comes from a 402 challenge ; preimage from paying the BOLT11 invoice in that same challenge. TTL 10 min. |
| Authorization: L402 deposit_<id>:<preimage_hex> | Multi-use deposit credit. id = 32-byte hex from POST /api/deposit ; preimage_hex = the preimage your wallet returned after paying the deposit BOLT11. TTL 30 days, decrements per call. |
Free endpoints take no auth.
Error codes
| HTTP | code | When |
|---|---|---|
| 400 | INVALID_PAYLOAD | Body fails Zod validation. error.issues contains the field-level details. |
| 400 | INVALID_URL_HASH | url_hash path param isn't 64-char hex. |
| 400 | INVALID_MACAROON_ID | macaroon_id path param isn't 64-char hex (after stripping deposit_ prefix). |
| 401 | DEPOSIT_AUTH_FAILED | Bearer preimage doesn't match the deposit's payment_hash. |
| 402 | PAYMENT_REQUIRED | Single-use L402 challenge ; pay the BOLT11 in WWW-Authenticate and retry. |
| 402 | PAYMENT_ALREADY_USED | This preimage was already redeemed. Pay a fresh invoice or use a deposit credit. |
| 402 | DEPOSIT_INSUFFICIENT | Deposit balance < price. Top up via POST /api/deposit. |
| 402 | DEPOSIT_EXPIRED | Deposit's 30-day TTL elapsed. Top up. |
| 404 | NOT_FOUND | Endpoint url_hash or deposit macaroon_id unknown. |
| 429 | RATE_LIMITED | Per-IP rate limit hit. See rate limits. |
| 503 | L402_NOT_CONFIGURED | LND not connected on the server. Operational issue ; retry later. |
| 503 | INTERNAL_ERROR | Postgres unavailable or unexpected exception. |
Rate limits
| Scope | Limit | Headers exposed |
|---|---|---|
| Global (every route) | 120 req / min / IP | RateLimit-Policy, RateLimit |
| POST /api/intent | 30 req / min / IP | (in addition to global) |
Keys are derived from X-Forwarded-For (Express trust proxy = 1). Expect 429 RATE_LIMITED with a clean JSON envelope when limits trip.
POST/api/intentL402 paid · 2 sats
Resolve a category + filters into a ranked list of L402 endpoint candidates with full Bayesian breakdown.
Request body
{
"category": string, // required, 1–64 chars
"keywords": string[], // optional, ≤10 entries, ≤40 chars each
"budget_sats": number, // optional, 1–10000 — drops endpoints priced higher
"max_latency_ms": number, // optional, 1–60000 — drops endpoints whose median latency exceeds
"optimize": "p_success" | "latency" | "cost", // optional, default "p_success"
"limit": number // optional, 1–20, default 10
}
Response 200
{
"data": {
"candidates": [
{
"url": string,
"url_hash": string, // sha256(url) hex
"category": string,
"name": string,
"description": string,
"http_method": "GET" | "POST" | "PUT" | "DELETE",
"price_sats": number,
"bayesian": { "p_success": number, "ci95": [number, number], "n_obs": number },
"stages": {
"challenge": { "p_success": number, "ci95": [number, number], "n": number },
"invoice": { ...same shape... },
"payment": { ...same shape... },
"delivery": { ...same shape... },
"quality": { ...same shape... }
},
"median_latency_ms": number | null,
"is_meaningful": boolean
}
// … up to `limit` candidates
],
"count": number
}
}
Example — single-shot 2-sat call
# 1. Trigger the L402 challenge.
curl -i -X POST https://satrank.dev/api/intent \
-H 'Content-Type: application/json' \
-d '{"category":"data","budget_sats":20,"optimize":"latency","limit":3}'
# → HTTP 402 with WWW-Authenticate: L402 macaroon="…", invoice="lnbc…"
# 2. Pay the BOLT11 with any LN wallet → wallet returns the preimage.
# 3. Replay with the bearer.
curl -X POST https://satrank.dev/api/intent \
-H 'Authorization: L402 <macaroon>:<preimage_hex>' \
-H 'Content-Type: application/json' \
-d '{"category":"data","budget_sats":20,"optimize":"latency","limit":3}'
Example — deposit credit (multi-call)
curl -X POST https://satrank.dev/api/intent \
-H 'Authorization: L402 deposit_<id>:<preimage_hex>' \
-H 'Content-Type: application/json' \
-d '{"category":"finance","optimize":"p_success"}'
# Each call decrements `sats_remaining` atomically. See POST /api/deposit.
Errors
400 INVALID_PAYLOAD, 402 PAYMENT_REQUIRED, 402 PAYMENT_ALREADY_USED, 402 DEPOSIT_INSUFFICIENT, 402 DEPOSIT_EXPIRED, 401 DEPOSIT_AUTH_FAILED, 429 RATE_LIMITED, 503 L402_NOT_CONFIGURED.
GET/api/services/bestfree · cached 5 min
Top-3 candidates per active category, ranked by p_e2e. Cached server-side for 5 minutes — agents can poll cheaply.
Response 200
{
"data": {
"<category>": [ <IntentCandidate>, <IntentCandidate>, <IntentCandidate> ],
"<category>": [ … ],
…
}
}
Same IntentCandidate shape as POST /api/intent.
Example
curl -s https://satrank.dev/api/services/best | jq 'keys'
# → ["ai", "analytics", "data", "developer-tools", "finance"]
GET/api/services/categoriesfree
List of categories with at least one endpoint, with the count.
Response 200
{
"data": [
{ "category": string, "count": number },
…
]
}
Example
curl -s https://satrank.dev/api/services/categories
# → {"data":[{"category":"data","count":18},{"category":"analytics","count":14},…]}
GET/api/services/:url_hashfree
Per-endpoint score snapshot — same Bayesian breakdown as in /api/intent candidates, scoped to one URL.
Path
| param | shape |
|---|---|
| url_hash | 64-char hex (sha256 of the endpoint URL) |
Response 200
{
"data": <IntentCandidate>
}
Errors
400 INVALID_URL_HASH, 404 NOT_FOUND.
Example
curl -s "https://satrank.dev/api/services/568a921b2014a977a38f68d151402e740e3af7fa985bfbb2272252d35bee7ab6" \
| jq '.data.bayesian'
# → {"p_success": 0.041, "ci95": [0, 1], "n_obs": 6}
POST/api/depositfree
Mint a multi-use deposit macaroon. Pre-pay N sats once, spend across many /api/intent calls without a Lightning round-trip per call. The endpoint itself is free — payment happens when you settle the BOLT11 it returns.
Request body
{
"sats": number // required, 10–10000. 100 sats covers ~50 paid calls at 2 sats each.
}
Response 200
{
"data": {
"macaroon": string, // "deposit_<hex64>" — pass to /api/intent as bearer
"invoice": string, // BOLT11 to settle from your LN wallet
"payment_hash": string, // sha256(preimage), 64-char hex
"sats": number, // initial credit
"expires_at": number, // Unix seconds, 30 days from now
"ttl_sec": number, // 2592000
"usage_hint": string
}
}
Errors
400 INVALID_PAYLOAD, 503 L402_NOT_CONFIGURED, 503 INTERNAL_ERROR.
Example
curl -s -X POST https://satrank.dev/api/deposit \
-H 'Content-Type: application/json' \
-d '{"sats":100}'
# → {"data":{"macaroon":"deposit_d68a…","invoice":"lnbc1…","payment_hash":"35aef0…",…}}
# Settle the invoice with any LN wallet → preimage returned.
# Then on every subsequent /api/intent:
# Authorization: L402 deposit_d68a…:<preimage_hex>
GET/api/deposit/:macaroon_idfree
Read remaining balance + activation status of a deposit macaroon. No auth — the macaroon_id is opaque enough to act as the read key.
Path
| param | shape |
|---|---|
| macaroon_id | 64-char hex (or full deposit_<hex>; the prefix is stripped) |
Response 200
{
"data": {
"macaroon_id": string,
"sats_initial": number,
"sats_remaining": number,
"issued_at": number,
"activated_at": number | null, // first call that succeeded
"expires_at": number,
"activated": boolean
}
}
Errors
400 INVALID_MACAROON_ID, 404 NOT_FOUND.
Example
curl -s https://satrank.dev/api/deposit/d68a1fb9…
# → {"data":{"sats_initial":100,"sats_remaining":80,"activated":true,…}}
GET/api/oracle/budgetfree
Last 24 hours of revenue (paid /api/intent + /api/deposit settlements) and paid-probe spend. Public transparency feed.
Response 200
{
"data": {
"since": number, // Unix seconds, 24h ago
"revenue_sats": number,
"probe_spend_sats": number,
"coverage_ratio": number // revenue / probe_spend, capped at 1.0 in the UI
}
}
Example
curl -s https://satrank.dev/api/oracle/budget
# → {"data":{"since":1778232492,"revenue_sats":116,"probe_spend_sats":1985,"coverage_ratio":0.058}}
GET/healthfree
Liveness probe. Returns flat { "status": "ok" } when the process is up. Used by Docker healthcheck.
Response 200
{ "status": "ok" }
GET/.well-known/satrank-keyfree
Oracle's Schnorr public key. Use to verify Nostr kind 30782 trust assertions offline (no network round-trip needed once the key is cached).
Response 200
{ "pubkey": "<64-char hex Schnorr pubkey>" }
Errors
503 with {"error":"oracle key not configured"} if the operator hasn't set NOSTR_PRIVATE_KEY.
Example
curl -s https://satrank.dev/.well-known/satrank-key
# → {"pubkey":"5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4"}
Machine-readable spec
OpenAPI 3.0 spec : /openapi.json. Drop into openapi-typescript, oapi-codegen, or any OpenAPI-aware client generator.
The spec covers all routes documented above with full request/response schemas. Servers list contains the production base URL ; override locally for self-hosters.