# webcap — pay-per-call web capture

webcap turns any URL into a screenshot (PNG/JPEG/PDF) or structured text/JSON,
paid per call in USDC over x402 (HTTP 402). No API keys, no accounts: any EOA
holding USDC can pay, gaslessly — the payer signs an EIP-3009
transferWithAuthorization and the CDP facilitator verifies and settles it
on-chain. You pay USDC only, never ETH gas.

## Deployment

- Base URL: https://webcap.shoutsid.fyi
- Network: USDC on Base — eip155:8453
- USDC asset: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
- Pay-to (merchant wallet): 0xB25572D7317eb98EBb39c45Da40eAAEA2A56c25e
- Facilitator: https://api.cdp.coinbase.com/platform/v2/x402 (x402 v2 "exact" scheme)
- Service descriptor: https://webcap.shoutsid.fyi/v1/x402/service (JSON catalog with bazaar extension)
- OpenAPI 3.1: https://webcap.shoutsid.fyi/openapi.json
- Agent skill file: https://webcap.shoutsid.fyi/skill.md

## Paid endpoints (x402)

| Method + path | Price | Returns |
| --- | --- | --- |
| POST /v1/x402/capture | $0.001 | Screenshot of one URL: base64 image (png/jpeg/pdf) + persistent public artifact URL |
| POST /v1/x402/extract | $0.01 | Structured content (title, headings, paragraphs, links, images, markdown) from the page's main content — nav/cookie/sidebar/footer excluded; one payment covers a batch of up to 50 URLs |
| POST /v1/x402/audit | $0.002 | SEO basics + link/OG health in one call (title, description, OG tags, link health) |
| POST /v1/x402/map-lite | $0.002 | Site map in one call: URL list from sitemap/robots plus a 1-hop same-host crawl (maxUrls up to 50, default 20) |
| POST /v1/x402/video | $0.005 | Scroll-capture of one URL as video (mp4/webm): base64 artifact |
| POST /v1/x402/analyze | $0.01 | AI-powered visual analysis: classification, accessibility, layout, entities, sentiment |
| POST /v1/x402/analyze/batch | $0.01 | Batch AI analysis (up to 10 URLs, one payment) |
| POST /v1/x402/watches/topup | $0.1–$1 | 100 scheduled re-capture runs for an existing watch (capture watch $0.1, extract watch $1) |

## Paid endpoints (credits rail, bearer token)

The same products are buyable without a signing key: register once
(POST /v1/register {"address"} → {apiKey}), fund with a plain USDC transfer
(POST /v1/invoice → {merchant, token, requiredUsdc}), then send
"Authorization: Bearer <apiKey>". Every call costs 1 credit and the charge is
refunded whenever the call does not return 200; an empty balance answers 402
with a 1-credit top-up invoice. Full spec: https://webcap.shoutsid.fyi/openapi.json
(tags: accounts).

| Method + path | Returns |
| --- | --- |
| POST /v1/extract | per-URL results + {creditsCharged, balance} |
| POST /v1/audit | audit report + {creditsCharged, balance} |
| POST /v1/map-lite | same-host URL list + {creditsCharged, balance} |
| POST /v1/video | video artifact + {creditsCharged, balance} |
| POST /v1/analyze | {task, result} + {creditsCharged, balance} |
| POST /v1/analyze/batch | per-URL results + {creditsCharged, balance} |
| POST /v1/watches/{id}/topup | +100 watch runs: capture pack 10 credits, extract pack 100 (x402-pack parity) |

### Request / response shapes

POST /v1/x402/capture
    {"url": "https://example.com", "format": "png"}   // format: png | jpeg | pdf (default: png)
    // options?: {"timeoutMs", "fullPage", "viewport": {"width", "height"}, "deviceScaleFactor", "isMobile", "userAgent", "stealth": true, "proxy", "waitFor", "actions", "maxContentWords", "auth": {"headers": {"authorization": "Bearer …"}, "cookies": [{"name", "value", "domain"?}]}}
  200 {"artifact": {"format": "png", "bytes": 123, "data": "<base64>", "url": "<base>/v1/artifacts/<uuid>"},
       "payment": {"payer": "0x…", "priceUsdcUnits": 1000}}

POST /v1/x402/extract
    {"url": "https://example.com"}                    // or "urls": string[] (up to 50) in one payment
    {"url": "https://example.com", "schema": "JSON with the fields: title, price"}  // optional schema-constrained extraction
  200 {"results": [{"url": "…", "status": "ok", "data": {"title": "…", "headings": […], "paragraphs": […], "links": […], "images": […], "markdown": "…"}}],
       "payment": {"payer": "0x…", "priceUsdcUnits": 10000}}

POST /v1/x402/map-lite
    {"url": "https://example.com"}                    // optional "maxUrls": 1–50 (default 20)
  200 {"urls": ["https://example.com/", "https://example.com/about"],
       "payment": {"payer": "0x…", "priceUsdcUnits": 2000}}

POST /v1/x402/video
    {"url": "https://example.com", "format": "mp4"}   // format: mp4 | webm (default: mp4)
    // options?: {"durationMs" (default 5000, at most 30000), "scrollSpeed" (default 800, at most 5000), "scrollEasing": "linear" | "ease-in-out", "viewport": {"width", "height"}, "auth"?: {"headers", "cookies"} (logged-in recordings), "stealth"?, "locale"?, "timezoneId"?}
  200 {"artifact": {"mime": "video/mp4", "bytes": 1048576, "data": "<base64>"},
       "payment": {"payer": "0x…", "priceUsdcUnits": 5000}}

POST /v1/x402/analyze
    {"url": "https://example.com", "task": "classification"}   // task: classification | accessibility | layout | entities | sentiment
    // optional "context": "focus on product pricing"
  200 {"task": "classification", "result": {"category": "e-commerce", "confidence": 0.92, "tags": ["shopping", "retail"]},
       "payment": {"payer": "0x…", "priceUsdcUnits": 10000}, "latency_ms": 1234}

POST /v1/x402/analyze/batch
    {"urls": ["https://a.com", "https://b.com"], "task": "classification"}   // up to 10 URLs, one payment
  200 {"results": [{"url": "…", "status": "ok", "result": {"category": "article", "confidence": 0.88}}],
       "task": "classification", "payment": {"payer": "0x…", "priceUsdcUnits": 10000}}

POST /v1/x402/watches/topup
    {"watchId": "<id from POST /v1/watches>", "runs": 100}
  200 {"watchId": "…", "credits": 100, "priceUsdcUnits": 100000}

## Free endpoints (no payment)

- **Rolling free allowance — the products are free, no wallet needed.** An unpaid POST to any product path (capture, extract, audit, map-lite, video, analyze) runs the real product and returns it. You get a window of free calls per caller IP; every response carries x-webcap-allowance-limit, x-webcap-allowance-remaining, x-webcap-allowance-cost, x-webcap-allowance-window-seconds and x-webcap-allowance-granted, so you always know where you stand. When the window is spent the same call answers the normal x402 402 challenge — pay and retry. No signature, no account, no API key. A free extract batch is capped (10 URLs); a single URL is not, and one paid call still covers up to 50. A call that fails does not consume your allowance.
- GET /v1/extract/preview?url=… — bounded structured preview (title, headings, links, truncated markdown); rate-limited per IP. A teaser: the real products are free inside the allowance above, so call those instead.
- GET /v1/x402/service — the machine catalog: every paid endpoint with its exact price, the network, USDC asset, payTo, facilitator and the how-to-pay flow.
- Tool manifests for framework wiring: https://webcap.shoutsid.fyi/.well-known/openai-tools.json (OpenAI functions shape) and https://webcap.shoutsid.fyi/.well-known/mcp-tools.json (MCP tools/list shape + the HTTPS endpoint per tool). Agent card (A2A v1.0): https://webcap.shoutsid.fyi/.well-known/agent-card.json.
- Remote MCP endpoint (no install, no package, no account): POST https://webcap.shoutsid.fyi/mcp — Streamable HTTP, JSON-RPC 2.0 (initialize, tools/list, tools/call). Point an MCP host at it: {"mcpServers":{"webcap":{"url":"https://webcap.shoutsid.fyi/mcp"}}}. Free tools run directly; this endpoint holds no wallet, so a paid tool call returns the live x402 402 challenge for you to settle with your own client.
- GET /v1/og?url=… — Open Graph metadata (title, description, image, icon)
- POST /v1/watches — create a scheduled re-capture watch (free; starts with 0 credits — top up via /v1/x402/watches/topup)
- GET /v1/watches/:id — watch state + recent runs · DELETE /v1/watches/:id — remove it
- GET /v1/health — liveness + chain info
- POST /v1/feedback — tell webcap something as an agent or human (message 8-4000 chars required; optional category bug|suggestion|pricing|docs|integration|other and the endpoint you were using). No account, rate-limited per client (60/hr), hashed payer. Human form: GET /feedback.

## Paying: minimal x402 flow

1. POST the endpoint unpaid → HTTP 402 with the x402 v2 challenge (JSON body, mirrored base64-encoded in the PAYMENT-REQUIRED header).
2. Read accepts[0] from the challenge ({ network, asset, amount, payTo, extra }) and sign a gasless EIP-3009 transferWithAuthorization: from = your EOA, to = payTo, value = amount (atomic 6-decimal USDC units), with the EIP-712 domain of the USDC contract (name/version from extra, chainId from network, verifyingContract = asset).
3. Retry the identical request with the header PAYMENT-SIGNATURE: <base64 payment payload>.
4. The facilitator verifies and settles on-chain; the 200 response carries the result plus a PAYMENT-RESPONSE settlement header (transaction hash, payer, amount).

Every paid path answers the challenge for GET as well as POST, and both are payable: POST takes the JSON body documented below, GET takes the same parameters in the query string (`GET /v1/x402/map-lite?url=…&maxUrls=5`, `GET /v1/x402/extract?url=…&options={"maxContentWords":800}`) — numeric and boolean parameters arrive typed, and arrays/objects are JSON-encoded, so the query stands in for the body. The challenge is advertised per method, so sign the challenge you were actually given and retry that same method: a payload signed for a POST is rejected on a GET (the bazaar extension echoes the request method).

Any x402 v2 client does steps 1–3 for you (e.g. @x402/axios with wrapAxiosWithPayment) — a ready-to-paste example lives in https://webcap.shoutsid.fyi/skill.md.

## Paying with MPP (optional second rail)

Every paid route also answers the unpaid 402 with a `WWW-Authenticate: Payment` challenge (Machine Payments Protocol, method="evm", realm = webcap.shoutsid.fyi) priced identically to the x402 `accepts[0]` terms — same amount, same USDC asset, same merchant wallet, same network. If your stack speaks MPP rather than x402, read that header and charge the same gasless EIP-3009 authorization through your MPP client. Deployments without MPP_SECRET_KEY are x402-only.

## Spend caps

Deployments may cap spend per payer (x402, atomic USDC units) or per account
(credits rail) via WEBCAP_SPEND_CAP_USDC_UNITS / WEBCAP_SPEND_CAP_CREDITS. Past
the cap, paid submits answer 429 spend_cap_exceeded with detail {payer, spent,
cap, reason}. Unset means unlimited: a cap that isn't configured can never block.

## URL safety (SSRF)

Targets pass a static allowlist guard. Blocked hosts 422 with
detail {reason, dnsRebindingCaveat: true}, so a policy block is distinguishable
from a malformed URL. The guard's own caveat, quoted from the source:

> NOTE: this is a static (parse-time) guard only. DNS rebinding — a hostname
> that resolves publicly here but to a private IP at fetch time — is a
> documented residual risk and must be re-checked where the actual fetch
> happens. Do not add async DNS resolution to this function.

## Async capture + signed artifacts

POST /v1/capture/jobs {"url", "format"?, "webhookUrl"?} charges once and
returns 202 {jobId, status}. Poll GET /v1/capture/jobs/{id} (free, no auth)
until completed|failed; terminal states carry the payment receipt (payer,
priceUsdcUnits, plus creditsUsed/costUsdcUnits where applicable). An optional
https webhookUrl gets the terminal delivery. Artifact URLs accept signed query
?exp=&sig= (HMAC-SHA256 over "<id>.<exp>"): bad signatures 403, expired ones
410; unsigned fetches still serve.

## Extraction notes

One extract payment covers the whole batch (up to 50 URLs). Asking again costs
again: the price stays flat per batch while compute scales per URL.

What comes back is the page's main content, not the whole document: navigation,
cookie banners, sidebars and footers are excluded from "paragraphs" and
"markdown", and the response's "content" field says which container was used,
how many words it holds, and whether a budget cut it short. Size the output to
your context window with "options": {"maxContentWords": N}.

A JSON
object schema takes the deterministic path (zero model calls, no model needed):
the response data gains an "extracted" projection of the page structure, and
optional "spans" [{field, quote, page}] ground each quote as a verbatim
markdown substring. Object-schema failures 422 with dollar-rooted detail
strings (schema mismatch, ungrounded spans, or unsupported keywords
oneOf/anyOf/allOf/$ref/format).
