# Bull.inf documentation

A two-sided marketplace for LLM inference — same product shape as [Surplus Intelligence](https://www.surplusintelligence.ai/docs), settled in **$ANSEM on Solana** instead of USDC on Base.

**Start here:** open [`/docs`](/docs) — buy/sell walkthrough on the same site as the market desk.

This file is the **API reference** (also at [`/docs.md`](/docs.md)).

Canonical local base: `http://127.0.0.1:8000`  
OpenAI-compatible base: `http://127.0.0.1:8000/v1`

## What is this?

- **Buyers** get one `inf_…` API key and call `/v1/chat/completions` like OpenAI. Bull.inf routes to the cheapest active seller offer. **You do not need your own lab key** for normal marketplace buys.
- **Sellers** list capacity (trusted allowlisted providers, or custom/unverified URLs with buyer opt-in), set a discount off catalog, and earn `$ANSEM` per request.
- **BYOK** is an optional buyer side-path (thin relay fee), not how you get Claude/GPT from the market.
- **Settlement** is Surplus-shaped: each marketplace completion pays the seller and platform fee in one on-chain `settle-request` (buyer ATA → seller + treasury). Optional **Load** remains for legacy ledger credit; admin `settle` / `settle-batch` clear backlog only.

| Surplus (reference) | Bull.inf |
|---|---|
| USDC on Base | **$ANSEM** SPL on **Solana** |
| Fund wallet → `approve(SettlementV2)` → spend | Fund wallet → **Approve** (treasury delegate) → spend |
| SettlementV2 `transferFrom` per request (buyer → seller + fee) | **Per-call** `settle-request`: one SPL tx pays seller + platform fee from buyer ATA (treasury as delegate). BYOK/fee-only still uses a single-leg pull to treasury |
| SettlementV2 contract | Treasury pubkey (`settlement_authority` on `GET /api/network`) |
| Seller USDC payout (immediate in settle tx) | Seller `$ANSEM` paid in the same per-call settle-request. Admin `settle` / `settle-batch` remain for **backlog** (promo subsidy, historical ledger, recovery) |
| `POST /v1/buyer/keys` | `POST /api/buy` (Privy) |
| `POST /v1/seller/offers` | `POST /api/sell` (Privy + optional `X-Seller-Token`) |
| `GET /v1/models` | **`GET /v1/models`** (OpenAI shape) + `GET /api/models` |
| `GET /api/markets` / `:model` / `feed` | Same paths under Bull.inf |

**Phase 1 parity (shipped):** Anthropic skin (`POST /anthropic/v1/messages`), BYOK priority providers (`/api/buyer/providers`), `GET /v1/prices`, and `min{N}/` discount routing on model ids.

**Phase 2 (shipped):** buyer usage export (`GET /api/buyer/usage`), savings series (`GET /api/buyer/savings`), spend/savings on `/api/me`, multi-key list/revoke (`/api/buyer/keys`), seller offer health probe + skip in routing, market desk UI for keys/BYOK/min{N}/savings.

**Agent x402 (shipped):** unauthenticated agents can pay per request with **`$ANSEM` off-chain vouchers** (`ansem-batch`). Many small payments aggregate off-chain; admin `POST /api/x402/settle-batch` pulls deltas on-chain in one batch. See [Agent quickstart (x402)](#agent-quickstart-x402).

Still out of scope vs Surplus (for now): MPP/Tempo, images/video/music/TTS/STT/embeddings, fiat onramps, EVM/USDC rails. Surplus-style async `pending_settlements` retry after a successful model response is also out of scope for the API-key path (settle stays sync fail-hard in `_finalize`).

---

## Agent quickstart (x402)

AI agents can pay per request with **no Bull account and no `inf_` key**. Settlement asset is **`$ANSEM` on Solana only** (not USDC / Base).

1. Agent `POST /v1/chat/completions` with no `Authorization`
2. Server returns **HTTP 402** with `PAYMENT-REQUIRED` (base64 JSON). Prefer scheme `ansem-batch`.
3. Agent Approves the treasury as SPL delegate once (same authority as API-key buyers — `payTo` / `GET /x402/info`).
4. Agent signs a **cumulative Ed25519 voucher** (`bull.x402.v1|…`) and retries with `PAYMENT-SIGNATURE`.
5. Server verifies the voucher off-chain and serves inference. **No Solana tx per call.**
6. Ops/admin runs `POST /api/x402/settle-batch` to pull `consumed − settled` from many payers in one (or few) txs.

Discovery: `GET /x402/info` (or `/api/x402/info`).

Enable with `X402_ENABLED=1`. Optional: `X402_FLAT_FEE_MICROS`, `X402_MAX_VOUCHER_MICROS`, `X402_BATCH_MAX_CHANNELS`.

Voucher message (ASCII, `|`-joined):

`bull.x402.v1|{channelId}|{payer}|{payTo}|{mint}|{cumulativeMicros}|{expiry}|{nonce}`

Sign with the payer’s Ed25519 secret; put the base58 signature in the payment JSON, then base64-encode the whole object into `PAYMENT-SIGNATURE`.

`exact` is advertised for compatibility but **rejected without on-chain proof** — use `ansem-batch`.

---

## Buyer quickstart

### 1. Sign in

Open `/market`, connect a Solana wallet via Privy. Access token auto-fills.

### 2. Get an API key

**Buy** panel → **Get key** (or):

```bash
curl -s http://127.0.0.1:8000/api/buy \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username":"optional"}'
# → { "api_key": "inf_…", "balance_ansem": 0, … }
```

First buy mints a key with **no free ledger ANSEM** by default (`BUYER_SIGNUP_GRANT` defaults to `0`). Deposit `$ANSEM` + SOL → **Approve** the treasury as SPL delegate before calls will succeed (otherwise `402`). Each LLM completion settles with one Solana tx that pulls `$ANSEM` from the deposit wallet (buyer pays SOL network fees). Legacy free-call modes (`PROMO_MODE=grant` / `zero_debit`) stay inactive unless `ALLOW_LEGACY_PROMO=1`. Optional **Load** remains for legacy ledger credit; the default Surplus path is Approve → per-call pull.

### 3. Fund wallet → Approve → call

Same shape as Surplus buyer funding, adapted for Solana. **Required** to fund API calls.

1. **Fund wallet** — hold `$ANSEM` + a SOL fee buffer in your Privy deposit wallet (you can send from Phantom without connecting it).
2. **Approve** — SPL Token Approve with the treasury as **delegate** (`settlement_authority` from `GET /api/network`). On `/market`, use **Approve** (you pay SOL to sign). Enable the Privy session signer so the API can co-sign fee payer on each call.
3. **Call** — `POST /v1/chat/completions` pulls the quoted `$ANSEM` on-chain per completion; SOL fee buffer covers network fees only (`GET /api/fees/solana` for live estimates).

```bash
# Live: wallet signs Approve off-chain (Phantom / Privy), then:
curl -s http://127.0.0.1:8000/api/deposit/load \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":25}'
# → { "credit_balance_ansem": …, "wallet_balance_ansem": …, "allowance_ansem": …, "tx_ref": … }

# Mock / local demo (no Phantom): simulate Approve, then Load
curl -s http://127.0.0.1:8000/api/deposit/mock-approve \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":25,"solana_wallet":"YourSolanaPubkey"}'
curl -s http://127.0.0.1:8000/api/deposit/load \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":10}'
```

`GET /api/me` (and `/api/buyer/me`) expose `wallet_balance_ansem`, `allowance_ansem`, and `credit_balance_ansem` like Surplus buyer/me.

**Admin fund** (ops / secondary path — still available on `/market` under Admin fund):

```bash
curl -s http://127.0.0.1:8000/api/fund \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username":"optional","amount":50}'
```

### 4. Call chat completions

```bash
curl http://127.0.0.1:8000/v1/chat/completions \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-4-8",
    "messages": [{"role": "user", "content": "Hello!"}],
    "stream": false
  }'
```

**Cursor / OpenAI SDK**

```
API Base URL: http://127.0.0.1:8000/v1
API Key:      inf_…
Model:        claude-opus-4-8
```

```python
from openai import OpenAI
client = OpenAI(api_key="inf_…", base_url="http://127.0.0.1:8000/v1")
client.chat.completions.create(
    model="claude-opus-4-8",
    messages=[{"role": "user", "content": "Hello!"}],
)
```

### 5. Check session

```bash
curl http://127.0.0.1:8000/api/me -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN"
curl http://127.0.0.1:8000/v1/models
curl http://127.0.0.1:8000/v1/prices
```

### 6. Claude Code / Anthropic SDK

Point Anthropic clients at Bull.inf’s skin (same `inf_…` key):

```
ANTHROPIC_BASE_URL=http://127.0.0.1:8000/anthropic
ANTHROPIC_API_KEY=inf_…
```

```bash
curl http://127.0.0.1:8000/anthropic/v1/messages \
  -H "x-api-key: inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-4-8",
    "max_tokens": 128,
    "messages": [{"role": "user", "content": "Hello!"}]
  }'
```

### 7. Minimum discount routing

Require marketplace offers to meet a floor discount (`min0`–`min100`, no leading zeros):

```bash
curl http://127.0.0.1:8000/v1/chat/completions \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "min40/claude-opus-4-8",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'
# → 503 {"error":"minimum_discount_not_met"} if no offer qualifies
```

### 8. BYOK (priority provider)

Register an OpenAI-compatible upstream; chat tries it before marketplace sellers. Successful BYOK calls debit a thin **relay fee** = marketplace quote × `BYOK_FEE_RATE` (default `0.02`); seller payout is `0` and offer capacity is not decremented. Your provider key is billed by the lab separately. Cold BYOK failure falls back to the marketplace seller (full quote + normal payout).

```bash
curl -s http://127.0.0.1:8000/api/buyer/providers \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider_base_url": "https://api.openai.com/v1",
    "provider_key": "sk-your-key",
    "model_id": "claude-opus-4-8",
    "label": "my-upstream"
  }'
# Omit model_id to apply to all models. List: GET /api/buyer/providers
# Delete: DELETE /api/buyer/providers/{id}
```

### 9. Routing preferences (trusted-only default)

By default, chat routes only to **trusted** seller upstreams (allowlisted hosts). Opt in to include **unverified** custom seller endpoints (`upstream_trusted=0`).

- **Account default:** `PATCH /api/buyer/routing` with `{ "allow_untrusted": true|false }`
- **Per API key:** `PATCH /api/buyer/keys/{key_hash}/routing` with `{ "allow_untrusted": true|false|null }` — `null` inherits the account default
- **Read:** `GET /api/buyer/routing` → `{ allow_untrusted, keys: [{ key_prefix, key_hash, allow_untrusted }] }`

```bash
# Read current preference
curl -s http://127.0.0.1:8000/api/buyer/routing \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN"

# Opt in to untrusted seller upstreams
curl -s -X PATCH http://127.0.0.1:8000/api/buyer/routing \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"allow_untrusted": true}'

# Force one key back to trusted-only while account allows untrusted
curl -s -X PATCH http://127.0.0.1:8000/api/buyer/keys/{key_hash}/routing \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"allow_untrusted": false}'
```

Untrusted offers show an **Unverified** badge on the market board.

---

## Seller quickstart

List **capacity you operate** (trusted host or your OpenAI-compatible URL). Closed lab APIs (OpenAI, Anthropic, Google) cannot be listed as seller supply — buyers should use BYOK for those.

### 1. Sign in (same Privy Solana session)

### 2. List an offer

**List offer** → choose a trusted provider (default), paste API key → Continue (discovers models) → set prices → List. Custom URL is advanced/unverified and requires buyer Routing opt-in.

```bash
# Trusted allowlisted host
curl -s http://127.0.0.1:8000/api/sell \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "my-seller",
    "provider_id": "openrouter",
    "provider_key": "sk-your-provider-key",
    "payout_wallet": "YourSolanaPubkeyBase58",
    "model_id": "claude-opus-4-8",
    "in_price": 9.0,
    "out_price": 45.0,
    "capacity": 50
  }'

# Custom / untrusted OpenAI-compatible gateway (e.g. OpenCode Zen)
curl -s http://127.0.0.1:8000/api/sell \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider_id": "custom",
    "provider_base_url": "https://your-gateway.example/v1",
    "provider_key": "sk-your-key",
    "payout_wallet": "YourSolanaPubkeyBase58",
    "model_id": "my-model",
    "in_price": 1.0,
    "out_price": 2.0,
    "capacity": 20
  }'
# First listing returns seller_token stk_… (shown once). Store it.
# Custom listings show as Unverified on the market board.
```

Discover models first (optional):

```bash
curl -s http://127.0.0.1:8000/api/seller/discover \
  -H "Authorization: Bearer $PRIVY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"provider_id":"custom","provider_base_url":"https://your-gateway.example/v1","api_key":"sk-…"}'
```

Updates for the same seller+model **upsert** discount/capacity. Relist with `X-Seller-Token: stk_…` if needed.

### 3. Get paid

Each marketplace completion pays your `payout_wallet` on-chain in the same settle-request tx as the buyer’s payment (Surplus-style). Platform fee goes to treasury. No owed ledger on the happy path.

**Backlog / recovery:** admin `settle-batch` still clears any leftover `seller_payouts` (promo subsidy, historical balances, failed migrations). Schedule a cron if you expect backlog:

```bash
# Backlog batch (omit body to auto-pick sellers by balance)
curl -s http://127.0.0.1:8000/api/settle-batch \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
# Optional: {"seller_ids": [1, 2], "limit": 20} — limit caps auto-pick and explicit seller_ids alike
# → { "ok": true, "tx_refs": ["…"], "settlements": […], "chunked": false, … }

# Single seller (debug / one-off)
curl -s http://127.0.0.1:8000/api/settle \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"seller_id": 1}'
```

View pending backlog: `GET /api/ledger` (seller Privy or admin).

---

## Market data (public)

```bash
# Best offer per model
curl http://127.0.0.1:8000/api/markets

# Order book for one model
curl http://127.0.0.1:8000/api/markets/claude-opus-4-8

# Live completion feed (anonymized)
curl http://127.0.0.1:8000/api/markets/feed

# Network / mint metadata
curl http://127.0.0.1:8000/api/network

# Live KPI stream (SSE)
curl -N http://127.0.0.1:8000/api/stats/stream
```

---

## Payments ($ANSEM on Solana)

| Mode | Env | Behavior |
|---|---|---|
| Demo ledger | `ANSEM_TRANSFER_MODE=mock` | Approve/Load/settle are mock; use `POST /api/deposit/mock-approve` for local demos without RPC |
| Live SPL | `ANSEM_TRANSFER_MODE=solana` + isolated signer configuration | Remote signer performs policy-checked delegate pulls and seller settles |

**Treasury** = settlement authority (Surplus SettlementV2 analogue). Exposed as `treasury` / `settlement_authority` on `GET /api/network`. Production live mode requires `ANSEM_SIGNER_URL`, `ANSEM_SIGNER_TOKEN`, and `ANSEM_TREASURY_PUBKEY`; the API refuses to boot with a local treasury key. The isolated signer accepts constrained `send`, `send-batch`, `pull`, and `settle-request` intents. Confirm `solana_ready: true` before Approve.

Deploy the signer as a separate private service using `Dockerfile.signer` (or `railway.signer.json`). Only that service receives `ANSEM_TREASURY_KEYPAIR`. Mount a persistent volume at `/data` for replay protection. The signer exposes only `GET /health` and authenticated `POST /v1/settlements`; it never accepts arbitrary serialized transactions. Restrict ingress to the API service through private networking when available.

Mint defaults to The Black Bull (`ANSEM_MINT`). Setup + wallet gotchas: `ENV.md` → Live Solana. Also `GET /api/network`.

**Seller payout:** marketplace completions use per-call `settle-request` (buyer → seller + fee). Schedule `POST /api/settle-batch` only for backlog / promo subsidy (e.g. every few minutes). Empty body settles up to `SETTLE_BATCH_LIMIT` sellers with the highest pending balances (`SETTLE_BATCH_MIN_MICROS` skips dust). Large batches may return multiple `tx_refs` (`chunked: true`).

Platform fee: `PLATFORM_FEE` (default `0.05`) — seller payout = buyer cost × (1 − fee).

BYOK relay fee: `BYOK_FEE_RATE` (default `0.02`) — on successful BYOK, buyer pays quote × rate; seller payout stays `0`.

---

## API surface (MVP)

| Method | Path | Auth |
|---|---|---|
| POST | `/v1/chat/completions` | Buyer `inf_…` |
| GET | `/v1/models` | Public |
| GET | `/v1/prices` | Public |
| POST | `/anthropic/v1/messages` | Buyer `inf_…` (`x-api-key` or Bearer) |
| GET/POST/DELETE | `/api/buyer/providers` | Privy (BYOK) |
| GET | `/api/buyer/usage` | Privy (JSON/CSV export) |
| GET | `/api/buyer/savings` | Privy (lifetime + weekly/daily USD series vs official API list) |
| GET/DELETE | `/api/buyer/keys` | Privy (list / revoke) |
| GET/PATCH | `/api/buyer/routing` | Privy (trusted-only default / opt-in) |
| PATCH | `/api/buyer/keys/{key_hash}/routing` | Privy (per-key routing override) |
| POST | `/api/seller/offers/{id}/probe` | Privy seller / admin |
| GET | `/api/seller/offers/health` | Privy seller |
| GET | `/api/markets` | Public |
| GET | `/api/markets/{model_id}` | Public |
| GET | `/api/markets/feed` | Public |
| GET | `/api/stats`, `/api/stats/stream`, `/api/stats/daily` | Public |
| GET | `/api/network`, `/api/config` | Public |
| POST | `/api/buy`, `/api/sell` | Privy |
| GET | `/api/me`, `/api/buyer/me`, `/api/ledger`, `/api/balance/{user}` | Privy / owner |
| POST | `/api/deposit/load` | Privy (delegate pull → ledger) |
| POST | `/api/deposit/mock-approve` | Privy (mock mode only) |
| POST | `/api/fund`, `/api/settle`, `/api/settle-batch` | Admin |
| GET | `/x402/info`, `/api/x402/info` | Public x402 discovery (`$ANSEM`) |
| POST | `/api/x402/settle-batch` | Admin — batch pull agent voucher deltas |
| POST | `/api/admin/clawback-signup-grants` | Admin (zero grant-only balances; `?dry_run=true` default) |
| GET | `/api/analytics` | Admin |

Security notes (aligned with Surplus intent): seller provider keys encrypted at rest (Fernet/`SURPLUS_SECRET`); SSRF checks on upstream URLs; API keys hashed; no prompt bodies stored — usage/cost metadata only.

---

## Local demo flags

```bash
export ADMIN_API_KEY=dev-admin
export ALLOW_INSECURE_DEV=1          # Bearer did:privy:… on user routes
export ALLOW_SYNTHETIC_UPSTREAM=1    # fake completions without a real gateway
export SURPLUS_SECRET=local-dev-secret
export ANSEM_TRANSFER_MODE=mock
cd backend && python -m uvicorn app:app --reload --port 8000
```

See `ENV.md` for production Privy + Solana settle requirements.
