Skip to main content

Developer Docs

L402 + MCP + WebLN. One scrollable page.

How It Works

Every paid endpoint uses the L402 protocol — a three-step challenge/response flow where the Lightning invoice is the credential. No API keys, no accounts, no signup.

C
Client / Agent
POST /api/models/image (no auth)
402 + WWW-Authenticate: L402 macaroon="...", invoice="lnbc..."
Pay invoice with any Lightning wallet → get preimage
POST + Authorization: L402 macaroon:preimage
200 OK + result (image URL, text, etc.)
S
Sats4AI
Request (no auth) 402 Challenge Authenticated
1. Request

Hit any endpoint without auth. You get back HTTP 402 with a Lightning invoice and a macaroon.

2. Pay

Pay the invoice with any Lightning wallet. The payment proof (preimage) is your credential.

3. Use

Resend with Authorization: L402 macaroon:preimage. Get your result.

Getting Started

Follow these steps to go from zero to your first API call.

  1. 1
    Get a Lightning wallet. Any wallet works: Phoenix, Breez, Muun, or a custodial wallet like Wallet of Satoshi. For agents, use Lightning Wallet MCP for automated payments.
  2. 2
    Pick your integration. Three options:
    • L402 (HTTP) — standard REST calls with Lightning auth. Works with any language.
    • MCP — add one line to your Claude/Cursor config. Payment negotiated in-protocol.
    • WebLN — browser apps with Bitcoin Connect or Alby.
  3. 3
    Discover services. Browse the service catalog, or let your agent discover tools programmatically:
    • GET /api/discover?q=generate portrait — semantic search
    • GET /api/estimate-cost?service=image — pricing before payment
    • GET /.well-known/l402-services — full machine-readable catalog
  4. 4
    Make your first call. Copy the quickstart example below, pay the invoice, and get your result. Most services cost 5-200 sats ($0.005-$0.20).

Quickstart — pick a flow

Four ways to use Sats4AI. Pick the one that matches your client.

L402 (HTTP + Lightning)

For agents and CLI clients. 402 challenge, pay, retry. Works with any language.

# 1. Hit the endpoint without auth → get a 402 + invoice
curl -i -X POST https://sats4ai.com/api/models/image \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a cat in a tophat"}'

# Response: 402 Payment Required
# WWW-Authenticate: L402 macaroon="...", invoice="lnbc..."

# 2. Pay the invoice with any Lightning wallet, get a preimage

# 3. Re-send with Authorization header
curl -X POST https://sats4ai.com/api/models/image \
  -H "Authorization: L402 <macaroon>:<preimage>" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a cat in a tophat"}'

Agent SDKs (zero protocol code)

Lightning Labs' L402sdk handles the full 402 flow for you — agent hits any Sats4AI endpoint, SDK pays the invoice automatically. Works with LND, CLN, or any NWC wallet (Alby, Mutiny, Phoenix).

// Lightning Labs L402sdk — Vercel AI SDK tools
// npm i @lightninglabs/l402-ai ai @ai-sdk/openai
import { createL402Tools, WasmL402Client, WasmBudgetConfig } from "@lightninglabs/l402-ai";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

// Connect the SDK to a Lightning backend (LND, CLN, NWC, or SwissKnife)
const client = WasmL402Client.withLndRest(
  process.env.LND_URL!,
  process.env.LND_MACAROON!,
  // Budget caps: perRequest=1000 sats, daily=50_000 sats
  new WasmBudgetConfig(1000, 0, 50_000, 0),
  100, // request timeout (s)
);

// Gives the agent l402_fetch / l402_get_balance / l402_get_receipts
const tools = createL402Tools({ client });

const result = await generateText({
  model: openai("gpt-4o"),
  tools,
  maxSteps: 5,
  prompt: "Generate an image of a cat in a tophat using https://sats4ai.com/api/models/image (model: nano-banana-2). Return the image URL.",
});
// The agent autonomously hits the 402, pays the invoice, retries with L402 auth.

MCP (Claude / Cursor)

Inline payment negotiation handled by the MCP transport. Plain HTTP can't do this.

// claude_desktop_config.json (or ~/.claude.json / Cursor)
{
  "mcpServers": {
    "sats4ai": {
      "url": "https://sats4ai.com/api/mcp"
    }
  }
}

// Then in your MCP client:
// 1. tools/call create_payment → returns Lightning invoice
// 2. Pay with any Lightning wallet
// 3. tools/call <tool_name> with the paymentId → results
//
// Or use lightning-wallet MCP for fully-automated payments.

WebLN (Browser)

For browser apps with Alby / Bitcoin Connect.

// Browser flow with Bitcoin Connect / Alby
import { requestProvider } from "@getalby/bitcoin-connect";

const res = await fetch("/api/charge?service=image&model=...");
const { invoice, paymentId } = await res.json();

const provider = await requestProvider();
await provider.sendPayment(invoice);

// Submit the work request
const fd = new FormData();
fd.set("paymentId", paymentId);
fd.set("prompt", "a cat in a tophat");
const out = await fetch("/api/models/image", { method: "POST", body: fd });

Payment Protocols

  • L402 — HTTP 402 + Lightning. Macaroon issued in WWW-Authenticate; pay invoice; resend with Authorization: L402 macaroon:preimage. /l402.
  • MCP — JSON-RPC 2.0 over Streamable HTTP at /api/mcp. Tools negotiate payment in-protocol; the agent never has to parse 402 responses. /mcp.
  • MPP — Machine Payment Protocol. Same surface as L402, slightly different header naming. Both are accepted on every paid endpoint.
  • WebLN — for browser flows. Use /api/charge to mint an invoice, pay via the wallet provider, then submit the work with the paymentId.
  • Refunds — every post-payment failure includes an LNURL-withdraw link in the refund field. No support tickets needed.

Macaroon Caveats

Macaroons we issue are fail-closed: any unrecognised caveat rejects the request. Currently enforced:

  • RequestPath — macaroon binds to the exact endpoint path used for the 402 challenge.
  • ExpiresAt — typically 10 minutes from issuance.
  • PaymentHash — bound to the Lightning invoice; preimage required.
  • Service + Model — bound to the model that priced the call. Auto-routed calls must be retried with the routed model id (see Auto-Routing below).

Error Codes

Every error response includes a machine-readable error_code in both the JSON body and the X-Error-Code header. Full catalog at /api/error-codes.

curl https://sats4ai.com/api/error-codes
# {
#   "version": 1,
#   "codes": {
#     "TIMEOUT": "Request or upstream provider timed out. Retry later.",
#     "CONTENT_FILTERED": "Output blocked by safety/content moderation. Rephrase the prompt.",
#     "L402_INVALID_PARAMS": "Pre-payment validation failed. No invoice was created; no sats charged.",
#     "L402_REFUND_ISSUED": "Response payload includes a refund object with an LNURL-withdraw link.",
#     ...
#   }
# }

# Every error response includes:
#   - JSON: { "error": "...", "error_code": "TIMEOUT" }
#   - Header: X-Error-Code: TIMEOUT

Async Jobs

Long-running services (audiobook, video, transcription, AI calls, 3D models) return HTTP 202 with a standard shape. Poll the poll_url at poll_interval_ms.

# 1. Pay + submit a long-running job
curl -X POST https://sats4ai.com/api/models/epub-audiobook \
  -H "Authorization: L402 ..." \
  -F "file=@book.epub" -F "voice=Ashley"

# Response: 202 Accepted
# {
#   "status": "queued",
#   "job_id": "abc123",
#   "poll_url": "https://sats4ai.com/api/models/epub-audiobook/status?id=abc123",
#   "poll_interval_ms": 3000,
#   "estimated_completion_ms": 600000
# }
# Headers: X-Job-Id, X-Poll-Url, X-Poll-Interval-Ms

# 2. Poll the status URL until status === "completed"
curl https://sats4ai.com/api/models/epub-audiobook/status?id=abc123

Webhooks / Callbacks

Skip the polling loop. Pass callback_url + callback_id on any async job and we'll POST you when it finishes. HMAC-signed, SSRF-validated, opt-in.

# OPT-IN — include callback_url + callback_id in your async request.
# Polling keeps working; the webhook is a supplement, not a replacement.

curl -X POST https://sats4ai.com/api/models/epub-audiobook \
  -H "Authorization: L402 ..." \
  -F "file=@book.epub" -F "voice=Ashley" \
  -F "callback_url=https://your-app.example.com/hooks/sats4ai" \
  -F "callback_id=user-42-job-7"

# Response: 202 Accepted
# {
#   "status": "queued",
#   "job_id": "abc123",
#   "poll_url": "...",
#   "callback_id": "user-42-job-7",
#   "callback_registered": true,
#   "callback_secret": "<per-job 64-hex HMAC secret>"
# }

# When the job finishes we POST your callback_url with:
#   Headers: X-Sats4AI-Signature: sha256=<hex>
#   Body: {
#     "job_id": "abc123",
#     "callback_id": "user-42-job-7",
#     "status": "completed" | "failed",
#     "result_url" | "error_code" + "error",
#     "timestamp": "2026-04-13T..."
#   }

# Verify the signature (Node):
#   const mac = crypto.createHmac("sha256", callback_secret)
#                     .update(rawBody).digest("hex");
#   const ok  = req.headers["x-sats4ai-signature"] === "sha256=" + mac;

# Validation:
#   - callback_url MUST be public HTTPS (no localhost / private ranges / IP literals in RFC1918)
#   - callback_id is OPAQUE, max 128 chars, no control chars
#     → do NOT embed PII or secrets; it is logged server-side
#   - rejection → response includes "callback_registered": false + a reason.
#     Your job still runs; poll poll_url instead.
#   - retries: at 0s / 5s / 30s. 4xx aborts. Best-effort (single instance).

Privacy note: callback_id is echoed back in our logs and the webhook body — treat it as an opaque correlation string, not a place to stash user data. Validation rejects http://, loopback, link-local, and RFC1918 hosts so an attacker can't point us at your metadata service.

Auto-Routing

For categories with multiple models (text, image, audio), pass "model": "auto" to let the server pick the best default. The chosen model id is returned in the X-Route-Model response header.

# Send model: "auto" — server picks the best for the category
curl -i -X POST https://sats4ai.com/api/models/image \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a cat","model":"auto"}'

# Response: 402 Payment Required
# X-Route-Model: nano-banana-2          ← chosen model id
# X-Route-Category: Image
# X-Error-Code: L402_AUTO_ROUTED

# IMPORTANT: on the paid retry, send the *routed* model id, NOT "auto":
curl -X POST https://sats4ai.com/api/models/image \
  -H "Authorization: L402 ..." \
  -d '{"prompt":"a cat","model":"nano-banana-2"}'

# Otherwise the macaroon's price-per-model caveat may reject the request.

URL-path model selection

Convenience for clients that prefer paths over body fields:

# Convenience: model in URL path
curl -X POST https://sats4ai.com/api/m/text-generation/gpt-oss-120b \
  -H "Content-Type: application/json" \
  -d '{"prompt":"hello"}'

# Forwards to /api/models/text-generation with model injected.
# Body field still wins if both are set.

Estimate Cost

Pre-payment quotes for budget-aware agents. No auth, no side effects. /api/estimate-cost with no params returns the catalog.

# Get a quote before paying
curl 'https://sats4ai.com/api/estimate-cost?service=text-to-speech&model=omnivoice&chars=1500'
# {
#   "service": "text-to-speech",
#   "amount_sats": 15,
#   "currency": "BTC",
#   "breakdown": { "type": "per-character", "chars": 1500, "chars_per_sat": 100, "model": "omnivoice" },
#   "error_code": "L402_ESTIMATE_ONLY"
# }

# List the catalog
curl https://sats4ai.com/api/estimate-cost

Request Deduplication

Identical POST bodies from the same IP within 30 seconds are returned from cache. Useful when an agent retries due to a network blip and you don't want to be charged twice.

# Identical POST within 30s returns the cached response (no new payment)
# Response headers:
#   X-Dedup: hit       ← cached
#   X-Dedup: miss      ← fresh

# Bypass the cache with X-No-Cache: true
curl -X POST https://sats4ai.com/api/models/image \
  -H "X-No-Cache: true" -d '{...}'

# Note: dedup is best-effort, single-instance, 30s TTL.
# For strong idempotency use a unique paymentId per job.

CORS / Browser Use

All payment + routing headers are explicitly listed in Access-Control-Expose-Headers so browser fetch() can read them.

# Headers exposed to browser fetch():
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-No-Cache,
  Payment-Signature, X-Cashu
Access-Control-Expose-Headers: WWW-Authenticate, Payment-Receipt,
  Payment-Required, X-Route-Model, X-Route-Category, X-Dedup,
  X-Job-Id, X-Poll-Url, X-Poll-Interval-Ms, X-Error-Code

# So in the browser:
const res = await fetch("/api/models/image", { method: "POST", body });
res.headers.get("WWW-Authenticate");   // ← visible
res.headers.get("X-Error-Code");       // ← visible

Services

30+ AI services. Per-service docs live at /l402 (curl + payment examples) and the MCP tool catalog at /mcp.

Machine-readable discovery:

Compound endpoints (recipes)

Chained pipelines with a single payment and an outcome-shaped output. Cheaper than chaining primitives manually for the common case.

  • transcribe_translate — audio → transcript → translation (119 target languages). Perfect for WhatsApp voice messages in a language you don't speak, or reading a meeting recorded in another language. Auto-detects source.
  • open_voice_bridge + voice_bridge_say / _poll / _end — live phone call where your LLM is the brain. PSTN + 602-lang TTS + ~100-lang STT.
  • epub_to_audiobook — EPUB/PDF/TXT → M4B audiobook in 600+ languages with optional translation.
  • extract_receipt — receipt image → structured JSON (merchant, total, line items).

Capability-first GitHub repos

Per-service landing repos with runnable curl, Python, and TypeScript examples. MIT-licensed. Use as drop-in starting points or reference implementations.

  • ai-caller — AI voice agent API (alternative to Vapi, Retell, Bland)
  • book-to-audiobook — EPUB/PDF/TXT to audiobook in 600+ languages, with optional translation
  • pay-per-use-fax — Send a fax with Lightning, no contract or monthly fee
  • fax-to-email — Receive faxes to your inbox, no monthly number rental

Voice Bridge — a la carte phone calls

Live phone calls where your LLM is the brain. Sats4AI supplies composable primitives: PSTN + streaming STT + TTS. You drive each turn. Conversation context stays on your side — we never see it.

Four endpoints: /open (pay + place call) → /poll (get transcripts) → /say (text or raw audio) → /end (hangup + refund). Deposit billing: ~10 sats/min US/CA, ~30 intl, ~80 rare. Unused time auto-refunded.

For a turnkey voice agent where we run the brain, use ai_call instead. Voice Bridge is for agents that already have a brain and just need a phone line, ears, and a mouth.

  • STT: Gladia primary (~100 languages, free first 10 h/mo), Deepgram Nova-3 failover.
  • TTS: OmniVoice (602 languages). BYO audio supported via audio_base64 + encoding: mulaw_8000 | pcm_l16_16000.
  • Coverage matrix: /api/l402/voice-bridge/coverage.
  • MCP tools: open_voice_bridge, voice_bridge_say, poll_voice_bridge, end_voice_bridge.

Choosing a Voice Tier (TTS)

Three tiers optimized for different use cases:

TierLanguagesPriceQualityWhen to Choose
OmniVoice Global602+100 chars/satGoodRare/underserved languages (Yoruba, Marathi, Twi, Cebuano). Widest coverage by far.
Inworld Premium~950 chars/satBest (ELO #1)English and major languages where quality matters most. Highest fidelity voice.
Minimax Studio~910 chars/satGreatVoice cloning. Use when you need a specific voice (your own, a character, a brand).

Choosing a Text Model (LLM)

Pass model: "auto" to let us route to the cheapest model that meets quality bar, or pick explicitly:

ModelMakerPrice (sats)When to Choose
Qwen3 32BAlibaba~0.001 sats/charCheapest. 119 languages, ultra-fast. Best for translation and multilingual tasks.
GPT-OSS 120BOpenAI~0.003 sats/charMid-tier. Ultra-fast, 131K context. Code gen, quick answers, everyday tasks.
Kimi K2.5Moonshot AI~0.01 sats/charSmartest. 1T params, 262K context, vision, thinking mode. Complex reasoning and analysis.

Recipes (Compound Outcomes)

Two kinds of services live on Sats4AI: primitives (single capability, one call — generate-image, translate-text, send-sms) and recipes (compound outcomes we assemble from multiple primitives so you don't have to — stateful, real-time, or multi-step). Both are first-class. Orchestrators that want fine control use primitives. Agents and users that want an outcome in one call use recipes.

Every discovery entry carries endpoint_type: "primitive" | "recipe". Filter on it to list only recipes:

curl https://sats4ai.com/.well-known/l402-services \
  | jq '.services[] | select(.endpoint_type == "recipe") | .id'

Recipes available today

RecipePrimitives chainedOutcome
extract-receiptOCR + LLMReceipt or invoice → structured JSON (merchant, line items, totals, tax, currency, category).
extract-documentpdf.js + OCR + layout analysisPDF or image → clean Markdown. Smart routing per page: text-layer when present, OCR when scanned, hybrid when mixed.
epub-audiobookparse + TTS + translate (optional) + assembleEPUB/PDF/TXT → M4B audiobook with chapter markers. Resumable, 602-language narration, optional translation before narration.
ai-callPSTN + STT + LLM + TTSSend an AI agent to make a two-way phone call. Our brain. Auto-retries, transcript + analysis returned.
voice-bridgePSTN + streaming STT + TTS (session)Real-time phone call where YOUR LLM is the brain. /open/poll/say/end. Conversation context never leaves your side.
send-faxFax transport + page accountingSend a fax (PDF URL or typed text) to any number worldwide. Optional cover page. Tiered pricing per page.
receive-faxFax receive + caller-ID match + email delivery (+ optional OCR)Open a 24h receive window at our shared number, matched by caller ID. Delivered to your email with optional OCR add-on.

When to prefer a recipe over chaining primitives

A recipe earns its existence when external orchestration is genuinely hard — real-time sync (voice-bridge, ai-call), cross-step state (epub-audiobook resumability, fax page accounting), regulatory bundling (fax), or a coverage/quality chain only we have end-to-end. If an agent could replicate the outcome with two independent calls, it stays a primitive. New recipes follow the naming pattern <outcome>-<input-or-format> (e.g., extract-receipt, not receiptExtractor).

Agent Wallets

AI agents need a Lightning wallet to pay invoices autonomously. These are complementary tools that give your agent a wallet to spend at Sats4AI:

WalletTypeBest For
L402sdkSDK (TS/Python/Go/Rust)Vercel AI SDK + LangChain agents. Auto-pays L402, built-in budgets. Supports LND, CLN, NWC.
Lightning Wallet MCPMCP toolClaude, Cursor, any MCP client. Fully automated L402 payments.
Alby MCPMCP toolAlby Hub users. Self-custodial.
lngetCLIShell scripts, CI pipelines. Auto-pays L402 invoices.
CLW CashCLI walletBitcoin CLI wallet purpose-built for AI agents.
Glow CloudREST APISelf-deployable wallet API (Breez Spark SDK). Deploy to Vercel free tier.

Any wallet that can pay a BOLT11 invoice works. The wallets above just automate the payment step so agents can operate without human intervention.

Production Checklist

Before going live, verify your integration handles these scenarios:

  • Handle refunds. Post-payment errors include a refund object with an LNURL-withdraw link. Surface this to users or redeem it programmatically.
  • Check error_code not just HTTP status. Use the X-Error-Code header or JSON error_code field to decide retry vs. rephrase vs. escalate. Fetch /api/error-codes once at startup.
  • Respect poll_interval_ms on async jobs. Polling faster wastes requests and may trigger rate limits.
  • Set a budget. If your agent auto-pays invoices, configure a per-call or daily spending limit in your wallet to prevent runaway costs.
  • Handle macaroon expiry. Macaroons expire after ~10 minutes. If payment takes longer, request a fresh invoice.
  • Use auto-routing carefully. When model: "auto" returns a routed model in X-Route-Model, use that exact model id on the paid retry. See Auto-Routing.
  • Validate callback signatures. If using webhooks, verify the X-Sats4AI-Signature HMAC before trusting the payload. See Webhooks.
  • Estimate costs first. For budget-sensitive flows, call /api/estimate-cost before committing to payment.

Troubleshooting

I paid the invoice but got "invalid macaroon"

The macaroon expires ~10 minutes after the 402 challenge. If you took too long to pay, request a new invoice by re-sending the original request without auth. Also verify you're sending both the macaroon and the preimage separated by a colon: Authorization: L402 macaroon:preimage.

My async job is stuck in "processing"

Some jobs (audiobooks, 3D models) can take several minutes. Check estimated_completion_ms in the 202 response. If a job exceeds 2x the estimate and is still processing, the upstream provider may have failed. The job will eventually time out and the response will include a refund LNURL-withdraw link.

I got L402_INVALID_PARAMS before paying

Parameters are validated before an invoice is created. No sats were charged. Check the error field for specifics — common causes: missing required field, unsupported model name, file too large, or invalid enum value. Use /api/discover or /.well-known/l402-services to see valid options.

CORS errors in the browser

All endpoints return Access-Control-Allow-Origin: * and expose payment headers. If you're seeing CORS errors, check that you're not sending custom headers that aren't in our allow list. See CORS / Browser for the full header list.

My agent keeps getting charged for retries

Request dedup caches identical POST bodies for 30 seconds. If your agent is retrying with a different body (e.g., re-serialized JSON with different key order), dedup won't match. Use the same exact request body for retries, or pass a unique paymentId for strong idempotency. See Request Dedup.

How do I get a refund?

Every post-payment error includes a refund field with an LNURL-withdraw link. Open it in any LNURL-compatible wallet to claim the refund. If a refund link is missing or expired, email support with the payment hash and error details.

Which model should I use?

Use model: "auto" and let the server pick. Or call /api/estimate-cost to compare pricing, and /api/l402/models for the full model list with capabilities. The /.well-known/l402-services manifest includes performance metadata (latency, reliability) per model.

Tor Access

Every endpoint is available as a Tor hidden service. No clearnet required, no IP logged.

Hidden service address:

j5tfaz7s7osapdbry4d2wb5usyhtcvrm7kutonliqq7sjv2c47lsgoid.onion

curl

curl --socks5-hostname 127.0.0.1:9050 \
  http://j5tfaz7s7osapdbry4d2wb5usyhtcvrm7kutonliqq7sjv2c47lsgoid.onion/api/discover

Node.js

import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://127.0.0.1:9050");
const res = await fetch(
  "http://j5tfaz7s7osapdbry4d2wb5usyhtcvrm7kutonliqq7sjv2c47lsgoid.onion/api/l402/generate-image",
  { method: "POST", agent, headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt: "a cat", model: "auto" }) }
);

Tor provides end-to-end encryption — http:// is correct and secure over .onion. Tor Browser users visiting sats4ai.com see an automatic “.onion available” banner via the Onion-Location header. See the announcement post for more details.

Security

Sats4AI handles Bitcoin Lightning payments. We take security seriously.

Reporting Vulnerabilities

Do not open a public GitHub issue. Email sats4ai@gmail.com with a description, reproduction steps, and impact assessment. We acknowledge within 48 hours and provide a status update within 7 days.

In scope

  • L402 authentication bypass or macaroon forgery
  • Payment logic errors (double-spend, underpayment acceptance, invoice reuse)
  • API endpoint vulnerabilities (injection, SSRF, IDOR)
  • Information disclosure (API keys, wallet credentials)
  • Denial of service against payment or API infrastructure

Full policy including safe harbor and out-of-scope items: /.well-known/security.txt | SECURITY.md