SatRank

API reference

Nine HTTP routes. 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.
v3.0 9 routes JSON L402

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 formUsed 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

HTTPcodeWhen
400INVALID_PAYLOADBody fails Zod validation. error.issues contains the field-level details.
400INVALID_URL_HASHurl_hash path param isn't 64-char hex.
400INVALID_MACAROON_IDmacaroon_id path param isn't 64-char hex (after stripping deposit_ prefix).
401DEPOSIT_AUTH_FAILEDBearer preimage doesn't match the deposit's payment_hash.
402PAYMENT_REQUIREDSingle-use L402 challenge ; pay the BOLT11 in WWW-Authenticate and retry.
402PAYMENT_ALREADY_USEDThis preimage was already redeemed. Pay a fresh invoice or use a deposit credit.
402DEPOSIT_INSUFFICIENTDeposit balance < price. Top up via POST /api/deposit.
402DEPOSIT_EXPIREDDeposit's 30-day TTL elapsed. Top up.
404NOT_FOUNDEndpoint url_hash or deposit macaroon_id unknown.
429RATE_LIMITEDPer-IP rate limit hit. See rate limits.
503L402_NOT_CONFIGUREDLND not connected on the server. Operational issue ; retry later.
503INTERNAL_ERRORPostgres unavailable or unexpected exception.

Rate limits

ScopeLimitHeaders exposed
Global (every route)120 req / min / IPRateLimit-Policy, RateLimit
POST /api/intent30 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/intent

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

paramshape
url_hash64-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

paramshape
macaroon_id64-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.