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.
Hit any endpoint without auth. You get back HTTP 402 with a Lightning invoice and a macaroon.
Pay the invoice with any Lightning wallet. The payment proof (preimage) is your credential.
Resend with Authorization: L402 macaroon:preimage. Get your result.
Getting Started
Follow these steps to go from zero to your first API call.
- 1Get 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.
- 2Pick 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.
- 3Discover services. Browse the service catalog, or let your agent discover tools programmatically:
GET /api/discover?q=generate portrait— semantic searchGET /api/estimate-cost?service=image— pricing before paymentGET /.well-known/l402-services— full machine-readable catalog
- 4Make 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 withAuthorization: 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/chargeto mint an invoice, pay via the wallet provider, then submit the work with thepaymentId. - Refunds — every post-payment failure includes an LNURL-withdraw link in the
refundfield. 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: TIMEOUTAsync 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=abc123Webhooks / 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-costRequest 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"); // ← visibleServices
30+ AI services. Per-service docs live at /l402 (curl + payment examples) and the MCP tool catalog at /mcp.
Machine-readable discovery:
- /.well-known/l402-services — L402 manifest
- /.well-known/mcp — MCP server manifest
- /api/mcp/discovery — MCP tool catalog with pricing + performance
- /api/discover?q=... — semantic search
- /api/l402/models — model + tier listing
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:
| Tier | Languages | Price | Quality | When to Choose |
|---|---|---|---|---|
| OmniVoice Global | 602+ | 100 chars/sat | Good | Rare/underserved languages (Yoruba, Marathi, Twi, Cebuano). Widest coverage by far. |
| Inworld Premium | ~9 | 50 chars/sat | Best (ELO #1) | English and major languages where quality matters most. Highest fidelity voice. |
| Minimax Studio | ~9 | 10 chars/sat | Great | Voice 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:
| Model | Maker | Price (sats) | When to Choose |
|---|---|---|---|
| Qwen3 32B | Alibaba | ~0.001 sats/char | Cheapest. 119 languages, ultra-fast. Best for translation and multilingual tasks. |
| GPT-OSS 120B | OpenAI | ~0.003 sats/char | Mid-tier. Ultra-fast, 131K context. Code gen, quick answers, everyday tasks. |
| Kimi K2.5 | Moonshot AI | ~0.01 sats/char | Smartest. 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
| Recipe | Primitives chained | Outcome |
|---|---|---|
| extract-receipt | OCR + LLM | Receipt or invoice → structured JSON (merchant, line items, totals, tax, currency, category). |
| extract-document | pdf.js + OCR + layout analysis | PDF or image → clean Markdown. Smart routing per page: text-layer when present, OCR when scanned, hybrid when mixed. |
| epub-audiobook | parse + TTS + translate (optional) + assemble | EPUB/PDF/TXT → M4B audiobook with chapter markers. Resumable, 602-language narration, optional translation before narration. |
| ai-call | PSTN + STT + LLM + TTS | Send an AI agent to make a two-way phone call. Our brain. Auto-retries, transcript + analysis returned. |
| voice-bridge | PSTN + 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-fax | Fax transport + page accounting | Send a fax (PDF URL or typed text) to any number worldwide. Optional cover page. Tiered pricing per page. |
| receive-fax | Fax 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:
| Wallet | Type | Best For |
|---|---|---|
| L402sdk | SDK (TS/Python/Go/Rust) | Vercel AI SDK + LangChain agents. Auto-pays L402, built-in budgets. Supports LND, CLN, NWC. |
| Lightning Wallet MCP | MCP tool | Claude, Cursor, any MCP client. Fully automated L402 payments. |
| Alby MCP | MCP tool | Alby Hub users. Self-custodial. |
| lnget | CLI | Shell scripts, CI pipelines. Auto-pays L402 invoices. |
| CLW Cash | CLI wallet | Bitcoin CLI wallet purpose-built for AI agents. |
| Glow Cloud | REST API | Self-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
refundobject with an LNURL-withdraw link. Surface this to users or redeem it programmatically. - ☐Check
error_codenot just HTTP status. Use theX-Error-Codeheader or JSONerror_codefield to decide retry vs. rephrase vs. escalate. Fetch/api/error-codesonce at startup. - ☐Respect
poll_interval_mson 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 inX-Route-Model, use that exact model id on the paid retry. See Auto-Routing. - ☐Validate callback signatures. If using webhooks, verify the
X-Sats4AI-SignatureHMAC before trusting the payload. See Webhooks. - ☐Estimate costs first. For budget-sensitive flows, call
/api/estimate-costbefore 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.onioncurl
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