A pay-per-call AI API. You send a request, the server answers 402 Payment Required with a price in satoshis and a
BSV address. You pay that address, send the request again with the transaction id, and get the answer. The endpoints
follow the OpenAI request and response shapes where they can.
Behind the API the server calls upstream providers: xAI (Grok) for chat, with optional OpenRouter models as fallbacks;
Atlas Cloud for images and video; Replicate for transcription and music. Prices are the upstream cost multiplied by
MARKUP_MULTIPLIER, converted to satoshis at the current BSV/USD rate.
There is also a small account system (email and password) with API keys and a prepaid credit balance, plus an admin page.
| Endpoint | Pays with | What it does |
|---|---|---|
GET /api/v1/models |
free | Chat models and services on offer |
GET /api/v1/services |
free | Fixed-price services with USD prices |
POST /api/v1/price |
free | Price quote: { model, messages } or { service, units } |
POST /api/v1/chat/completions |
BSV or API key | Chat (OpenAI-compatible, stream supported) |
POST /api/v1/images/generate |
BSV or API key | One image: { prompt, model?, width?, height? } |
POST /api/v1/video/generate |
BSV or API key | Starts a video job: { prompt, model?, image_url?, duration? } |
POST /api/v1/transcriptions |
BSV or API key | Whisper: { audio_url, language? } |
POST /api/v1/music/generate |
BSV or API key | MusicGen: { prompt, duration? } (max 30 s) |
Chat model ids come from GET /api/v1/models. The aliases b/chat, b/fast and b/smart try a list of models in
order and fall back to the next one on outages (see lib/router.ts).
- Call a paid endpoint with no payment. The reply is
402with a JSON body (satoshisRequired, a breakdown) and headers:x-bsv-payment-satoshis-requiredx-bsv-payment-address(the server's receiving address)x-bsv-payment-network(mainnet)
- Broadcast a transaction that pays at least that many satoshis to that address.
- Repeat the same request with the header
x-bsv-payment: <txid>(or{"txid":"<txid>"}). - The server looks the transaction up on WhatsOnChain, adds up the outputs to its address, and checks the total
covers the price. Each txid can be used once (stored in
bsvapi_used_txids). If the upstream call then fails, the txid is released so you can retry with the same payment.
For chat, the price covers an output budget, and max_tokens is capped to that budget.
With an API key instead (Authorization: Bearer bsvapi_sk_...), the price is taken from the account's credit balance
before the call and refunded if the upstream call fails. Non-streaming chat is then charged for the tokens actually
used.
Requirements: Node 18+, pnpm, a Postgres database with the Supabase REST API in front of it (self-hosted Supabase or Supabase cloud), and keys for whichever upstream providers you want to offer.
-
Install:
pnpm install
-
Create the tables and functions:
psql "$DATABASE_URL" -f supabase/migrations/0001_init.sql -
Configure. Copy
.env.exampleto.env.localand fill it in. The server reads:Variable Needed for NEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEYEverything (database) JWT_SECRETSign-in sessions. At least 32 characters ( openssl rand -hex 32); the server refuses to sign or check sessions without itBSVAPI_HOT_WALLET_ADDRESSThe address callers pay. Only the address; keep its private key offline. pnpm tsx scripts/generate-hot-wallet.tsmakes a new oneXAI_API_KEY,XAI_BASE_URLGrok chat models OPENROUTER_API_KEYOpenRouter fallback models (optional) ANTHROPIC_API_KEYReserved for the router; no Anthropic models are listed yet ATLASCLOUD_API_KEY,ATLASCLOUD_URLImages and video REPLICATE_API_TOKENTranscription and music ADMIN_API_KEYOptional bearer key for /admin; ignored unless at least 32 characters. Without it, only signed-in users withis_admin = truecan use the admin routesMARKUP_MULTIPLIERPrice multiplier over upstream cost (default 2) -
Run:
pnpm dev # development pnpm build && pnpm start
Rate limits are counted per IP using the first x-forwarded-for entry. Vercel sets that header itself. Behind any
other proxy, make sure the proxy overwrites the header rather than appending to it, or callers can pick their own IP.
The plan is to remove the server-side money entirely:
- each paid call is paid from the caller's own wallet (x402, approved in the bWalletX payment sheet, or any BRC-100 wallet),
- no stored balances and no API-key credit accounts,
- no server hot wallet receiving user funds: each call's payment goes straight to the payee named in that call's 402 offer, bound to that one request.
Until then the current payment flow above is what the code does.
Please report security problems privately to: SECURITY CONTACT: owner to fill in before publishing.
Do not open a public issue for a vulnerability.
Known limits of the current design (being addressed in the rebuild):
- A payment is a bare txid. It is not bound to a particular request or caller, so someone watching the receiving address could submit another caller's txid first. Unconfirmed (0-conf) transactions are accepted.
- Transcription is charged one minute regardless of audio length; video and music are flat-priced per call.
- API keys are stored in the database as plain text.
- Credit changes use compare-and-swap from the app. A database function with a
credits_sats >= 0check would be stronger.
Open BSV License version 4. See LICENSE.