# shelf-smoke > Machine-buyable URL smoke for other AI bots. Live host `https://shelf.kymac.co`. > **Sole advertised entry:** this file (`GET /llms.txt`). Do not start at OpenAPI, MCP, or `/`. > Registry id: `co.kymac.shelf/shelf-smoke`. Capability keywords: url smoke, headless qa, viewport smoke. Bot-alone path: this file → get key → quote → free-try smoke. No human mid-loop. Prepaid checkout is only after the free credit is spent. GET /health is public. It reports `billing` (`test` | `live` | `gated`) and `runner_ready` (Playwright/Chromium can launch — not just process-up). Probe fails closed when `runner_ready` is false. Live Stripe stays gated (`unlock-live`); current hosts report `test` when TEST secrets are present, otherwise `gated`. Issue a caller key at POST /keys (`ssm_…` shown once). First issue grants 1 free credit (`credit.grant` reason=`first_key`). Prefer `Authorization: Bearer ssm_…` on gated routes. `X-API-Key: ssm_…` is an alias for the same secret. Key `id` is the stable `caller_id` for Ledger events. Install sequence (no human mid-loop): 1. GET `/llms.txt` (this file — sole advertised entry) 2. POST `/keys` — secret `ssm_…` returned once; `id` is `caller_id`. First issue grants 1 free credit. 3. Authenticate with `Authorization: Bearer ssm_…` (`X-API-Key` is an alias only) 4. GET `/credits/quote?op=smoke` — `cost`, `balance`, `would_succeed` without debit 5. POST `/smoke` against the golden URL `https://example.com/` 6. GET `/credits/packs` then checkout/confirm only after the free try is exhausted 7. GET `/credits/balance` and GET `/credits/events` — Ledger metering-truth ## Golden proof Known-good URL: `https://example.com/` Every `POST /smoke` HTTP 200 includes `credits_burned` and `credits_remaining`. Pass (`HTTP 200`; compare `ok`; burns 1 credit): ``` {"ok":true,"url":"https://example.com/","viewports":[{"name":"desktop","width":1280,"url":"https://example.com/","ok":true,"status":200},{"name":"phone","width":390,"url":"https://example.com/","ok":true,"status":200}],"credits_burned":1,"credits_remaining":0} ``` Fail (`HTTP 200`, `ok:false`; runner finished — failed smokes do not debit). Example when a URL returns HTTP 404: ``` {"ok":false,"url":"https://example.com/missing","viewports":[{"name":"desktop","width":1280,"url":"https://example.com/missing","ok":false,"status":404,"error":"HTTP 404"},{"name":"phone","width":390,"url":"https://example.com/missing","ok":false,"status":404,"error":"HTTP 404"}],"credits_burned":0,"credits_remaining":1} ``` HTTP 200 always means the runner finished — read `ok` for the verdict. ## Fail shapes Every HTTP fail body has `code`, `message`, `retryable`, and `error` (alias of `message`). No bare text 500s. 401 on gated routes (`WWW-Authenticate: Bearer` on both): - missing: `{"code":"missing_api_key","message":"missing API key","retryable":false,"error":"missing API key"}` - invalid: `{"code":"invalid_api_key","message":"invalid API key","retryable":false,"error":"invalid API key"}` 402 when smoke would run without enough credits: - `{"code":"insufficient_credits","message":"insufficient credits","retryable":false,"error":"insufficient credits","caller_id":"k_…","balance":0,"required":1}` 400 invalid request: `{"code":"invalid_request","message":"…","retryable":false,"error":"…"}` 404 unknown route: `{"code":"not_found","message":"not found","retryable":false,"error":"not found"}` 500 unhandled: `{"code":"internal_error","message":"…","retryable":true,"error":"…"}` 504 timeout: `{"code":"timeout","message":"smoke timed out","retryable":true,"error":"smoke timed out"}` 503 Stripe TEST missing: `{"code":"stripe_not_configured","message":"stripe test mode not configured","retryable":true,"error":"stripe test mode not configured"}` Ledger events (append-only, this `caller_id`): - `credit.grant` — caller_id, credits_added, reason (`first_key`), ts - `credit.purchase` — caller_id, pack_id, credits_added, amount_cents, currency, stripe_payment_intent_id (TEST), ts - `credit.debit` — caller_id, credits_burned, reason (`smoke_success`), request_id, ts - `credit.balance_snapshot` (optional) — caller_id, balance, ts (or derive from events) Stripe TEST only (`STRIPE_SECRET_KEY=sk_test_…`). Live keys fail closed (unlock-live). MCP Streamable HTTP is `POST /mcp` (not an install entry). Machine transport discovery: `GET /.well-known/mcp.json`. Copy-paste client snippet: `GET /mcp.json` (this file is the only advertised pointer to the snippet; not an install entry). Tools: `issue_key`, `verify_key`, `quote_credits`, `list_credit_packs`, `checkout_credits`, `confirm_credits`, `credit_balance`, `credit_events`, `smoke`, plus optional `health`. `smoke` returns the same credit fields and fail JSON as HTTP. Gated tools prefer `Authorization: Bearer` or env `SHELF_SMOKE_API_KEY`; `X-API-Key` is an alias. Stdio: `npx tsx src/mcp-stdio.ts` with `SHELF_SMOKE_API_KEY`.