v2 · x402-native

Skim API documentation

Turn any URL into clean, agent-ready markdown with one HTTP request. No signup, no API keys — pay per call in USDC over the x402 protocol.

Quickstart

Skim is built on x402— an open HTTP 402 payment protocol designed for autonomous agents. Point any x402-aware HTTP client at the endpoint, fund the wallet with USDC on Base, and your agent can read the web.

Not writing the code yourself? If you're building with an AI agent (vibe coding), you can hand it this page and ask it to set up clean web reads with Skim for you — these docs are written to be read by agents, so yours can do the wiring while you watch. You can even ask it to create and fund a wallet for itself.

Want to see the output before wiring anything? Try Skim free in your browser — 10 free skims a day, no wallet, no signup. Paste a URL, see exactly what your agent would get back.

Make your first request

# 1. First call — server replies 402 Payment Required with x402 instructions
curl -i -X POST https://skim402.com/api/v1/read \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/article" }'

# 2. Sign and submit payment with the x402 CLI or any x402 client,
#    then retry the same request with the X-PAYMENT header set.

curl -X POST https://skim402.com/api/v1/read \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{ "url": "https://example.com/article" }'

The code samples assume your wallet is already funded. If it isn't, the next section walks you through it. It takes about a minute, one time.

Fund your wallet

Skim has no signup, no API key, no account. Your wallet is your identity. Pay $0.002, get one clean read. That's the whole contract.

To pay, your client needs a Base wallet with a little USDC in it. Three steps.

1. Create a fresh wallet

Use a new wallet, not your personal one. It only ever spends — if a key leaks, the worst case is someone burning your read budget. Generate one in code:

import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);

console.log("Address:    ", account.address);
console.log("Private key:", privateKey); // save this somewhere safe

Or open Coinbase Wallet, hit "Create new wallet," save the seed phrase, and export the private key. Whichever you prefer.

2. Send it USDC on Base

Any source works. Coinbase exchange, a DEX, a bridge from another chain. Send to the address you just generated, on the Base network. A dollar funds about 500 basic reads.

USDC contract on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.

3. Paste the key into your env

# .env
AGENT_PRIVATE_KEY=0x...          # for the API code samples above
SKIM_WALLET_PRIVATE_KEY=0x...    # for the MCP server below

The key never leaves your machine. Your x402 client uses it locally to sign EIP-3009 USDC payment authorizations. Skim's facilitator settles each authorization on chain after the read succeeds. We never see the key. We never need it.

Set a ceiling per call so a buggy agent can't drain the wallet: the MCP server exposes SKIM_MAX_PRICE_USD (default 0.01), and most x402 clients accept a max-payment argument. Skim's current price is well under that.

Use from Claude / Cursor (MCP)

The fastest way to give Claude, Cursor, Codex, Cline, or any other MCP-compatible client the ability to read the open web is to install skim-mcp. It's a tiny stdio server that wraps Skim's /v1/readendpoint, handles the x402 payment handshake locally with your wallet, and exposes a single read_url tool to the agent.

You'll need a Base wallet private key with a little USDC in it. A dollar funds ~500 reads. Use a fresh wallet — not your personal one.

Install in Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows:

{
  "mcpServers": {
    "skim": {
      "command": "npx",
      "args": ["-y", "skim-mcp"],
      "env": {
        "SKIM_WALLET_PRIVATE_KEY": "0xYOUR_BASE_WALLET_PRIVATE_KEY"
      }
    }
  }
}

Restart Claude Desktop. You'll see a new read_url tool. Ask Claude to read any article and it'll fetch it through Skim and pay automatically.

Install in Cursor

Edit ~/.cursor/mcp.json (or use the in-app Settings → MCP panel) with the same JSON block as above.

Install in Codex

OpenAI's Codex CLI uses a TOML config instead of JSON. Add this to ~/.codex/config.toml:

[mcp_servers.skim]
command = "npx"
args = ["-y", "skim-mcp"]
env = { "SKIM_WALLET_PRIVATE_KEY" = "0xYOUR_BASE_WALLET_PRIVATE_KEY" }

Restart Codex. The read_url tool becomes available and Codex pays per read automatically through your wallet.

Install in Cline / Continue / Zed / other MCP clients

All MCP-compatible clients use the same shape. Run the binary as npx -y skim-mcp with SKIM_WALLET_PRIVATE_KEY set in the environment. See your client's MCP server config docs for the exact JSON location.

Environment variables

SKIM_WALLET_PRIVATE_KEYstring
required
Hex private key for the Base wallet that pays for reads. With or without 0x prefix. Never leaves your machine — only used locally to sign EIP-3009 payment authorizations.
SKIM_MAX_PRICE_USDstring
Maximum USD per call. Default 0.01. Caps how much the wallet will sign for in a single read. Skim is currently $0.002/call, well under this.
SKIM_API_URLstring
Override the API base URL. Default https://skim402.com. Mostly useful for local development.

Links

skim-mcp on npm · Source on GitHub · Model Context Protocol

Payment (x402)

The first request from an unpaid client returns 402 Payment Required with a JSON body describing the payment requirement — amount, asset (USDC), network (Base), and recipient address. An x402 client signs the payment, submits it to a facilitator, and retries the request with the X-PAYMENT header.

Example 402 response

HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "2000",
      "resource": "https://skim402.com/api/v1/read",
      "description": "Skim: clean reader — URL to markdown + metadata",
      "mimeType": "application/json",
      "payTo": "0x...",
      "maxTimeoutSeconds": 60,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
    }
  ]
}

Supported clients

Skim works with any x402-compliant client, including x402-axios, x402-fetch, the Python x402 SDK, Coinbase's AgentKit, Anthropic's MCP x402 connector, and the Vercel AI SDK x402 integration.

POST /v1/read

Fetches a URL and returns clean markdown plus structured metadata.

Request body

urlstring
required
The URL to fetch. Must be an absolute http(s):// URL. Private, loopback, and link-local addresses are rejected (SSRF protection).
modeenum
Only basic is accepted on this endpoint (and it's the default — you can omit the field). JavaScript-rendered pages use the separate /v1/read/js endpoint; schema-typed JSON uses /v1/extract. See Reader modes for the full breakdown.
stripLinksboolean
Optional, defaults to false. When true, link markup is flattened to its anchor text — [label](url) becomes label — so link-heavy pages (wikis, reference articles) return far smaller markdown. Headings, lists, and code blocks are untouched.
stripImagesboolean
Optional, defaults to false. When true, image markup (![alt](url)) is removed entirely. Set both flags for the leanest markdown. The plain text field is never affected by either flag.
chunkedboolean
Optional, defaults to false. When true, the response additionally includes a chunks array — the markdown pre-split into RAG-ready sections along heading boundaries, so you can feed them straight into an embedding pipeline without writing your own splitter. Free — the price doesn't change. See RAG chunking below.

Response

{
  "url": "https://example.com/article",
  "finalUrl": "https://example.com/article",
  "mode": "basic",
  "markdown": "# How agents read the web\n\nThe modern web is a mess...",
  "text": "How agents read the web. The modern web is a mess...",
  "metadata": {
    "title": "How agents read the web",
    "byline": "Jane Doe",
    "siteName": "Example",
    "publishedAt": "2025-08-15T00:00:00Z",
    "excerpt": "Why clean reader APIs matter for autonomous agents.",
    "lang": "en",
    "length": 4821
  },
  "fetchedAt": "2026-05-16T18:42:11Z"
}

Skim follows redirects (with re-validation at every hop). finalUrl is the URL that was actually read after redirects — if it differs from the url you requested, you were redirected. Agents that cite sources should compare the two and cite finalUrl.

When a read succeeds but the result looks suspicious, the response also carries a warnings array (omitted otherwise). Each entry has a machine-readable code and a human-readable message. The only current code is THIN_CONTENT: extraction produced fewer than 100 words, which usually means a JS app shell, login wall, or paywall rather than a real article. Treat flagged results as low confidence — for basic reads, retrying with mode: "js" may recover pages that need JavaScript.

Response headers

X-Cachestring
HIT if the response was served from Skim's 60-second in-memory cache, MISS otherwise. Cache keys include the URL and mode; payment is still required on cache hits (caching saves latency and our upstream cost, not the wallet).
X-Payment-Responsestring
Base64-encoded settlement receipt from the x402 facilitator, emitted on 2xx after payment settles.
X-Skim-Fallbackstring
Set to js when a basic /v1/read came back empty and Skim transparently re-rendered the page in a real browser. You still pay only the basic $0.002 rate — the browser render is on us. The response mode field also reads "js" in that case.

POST /v1/read/batch

Reads up to 10 URLs in a single paid call. Each URL is fetched concurrently (capped at 5 in-flight). One x402 payment covers the whole batch — priced at 10x the per-call rate ($0.020), so it's the same per URL when you actually use 10, and saves your agent from signing 10 separate payments.

Request body

urlsstring[]
required
Array of 1–10 absolute http(s):// URLs. Same SSRF protections as /v1/read.
modeenum
Only basic is supported on batch in v1. For js or structured reads, call /v1/read or /v1/extract per URL.
stripLinksboolean
Optional, defaults to false. Applies to every result in the batch — same behavior as on /v1/read.
stripImagesboolean
Optional, defaults to false. Applies to every result in the batch.
chunkedboolean
Optional, defaults to false. When true, every successful item includes a chunks array — see RAG chunking. Free.

Example

curl -X POST https://skim402.com/api/v1/read/batch \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{
    "urls": [
      "https://example.com/article-1",
      "https://example.com/article-2",
      "https://example.com/article-3"
    ]
  }'

Response

{
  "results": [
    {
      "url": "https://example.com/article-1",
      "ok": true,
      "data": { /* same shape as /v1/read result */ },
      "error": null
    },
    {
      "url": "https://example.com/article-2",
      "ok": false,
      "data": null,
      "error": { "status": 422, "message": "Upstream responded 404" }
    },
    {
      "url": "https://example.com/article-3",
      "ok": true,
      "data": { /* ... */ },
      "error": null
    }
  ],
  "fetchedAt": "2026-05-22T10:14:02Z"
}

Partial failures and refunds

Per-URL failures (bad URL, paywall, JS-only page) are surfaced inside the results array — they do not fail the whole batch, and you still pay for the batch as a whole. If every URL in the batch fails, the endpoint returns 422 and the x402 facilitator skips settlement entirely — your wallet is untouched.

POST /v1/read/js

Same idea as /v1/read, but the page is rendered with a real headless browser before extraction. Use this for SPAs, React/Vue/Svelte apps, and pages whose content is injected after initial load.

Priced at $0.005 per render (vs$0.002 for basic) because each call uses upstream browser time. Output shape is identical to /v1/read, with mode: "js" in the response.

Request body

urlstring
required
The URL to render. Same http(s):// and SSRF rules as /v1/read.
stripLinksboolean
Optional, defaults to false. Same behavior as on /v1/read.
stripImagesboolean
Optional, defaults to false. Same behavior as on /v1/read.
chunkedboolean
Optional, defaults to false. Adds a chunks array of RAG-ready sections — see RAG chunking. Free.

Example

curl -X POST https://skim402.com/api/v1/read/js \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{ "url": "https://my-spa.example.com/dashboard" }'

When to use it (and when not to)

Try /v1/read first — it's 2.5x cheaper and works on the majority of the web (blogs, docs, news, most marketing sites). Only fall back to /v1/read/js when basic returns 422 No readable content on a page you know is real. Common JS-rendered targets: React/Next.js dashboards, Twitter/X profiles, LinkedIn pages, Notion public pages.

RAG chunking

Every read endpoint (/v1/read, /v1/read/js, /v1/read/batch, and their v2 counterparts) accepts an optional chunked: true flag. When set, the response additionally carries a chunks array: the markdown pre-split into embedding-ready sections, so a RAG ingestion pipeline can skip its own text splitter and go straight from one paid call to vectors. It's free — the price of the read doesn't change.

How Skim splits

Chunks follow the document's own structure: the markdown is split at heading boundaries (code-fence aware — a # inside a code block is never treated as a heading, and a code block is never cut in half). Tiny sections are merged into their neighbor so heading-dense pages don't produce confetti; very long sections are split at paragraph boundaries at roughly 500 words, in which case consecutive chunks share the same heading. Concatenating every chunk's markdown reproduces the full document up to whitespace normalization — no words are ever added, dropped, or reordered.

Chunk shape

{
  "chunks": [
    {
      "index": 3,
      "heading": "3xx redirection",
      "path": ["Standard codes"],
      "markdown": "### 3xx redirection\n\nThis class of status code...",
      "wordCount": 421,
      "approxTokens": 988
    }
  ]
}
indexinteger
Zero-based position in document order.
headingstring | null
The section's own heading text, or null for content before the first heading.
pathstring[]
Breadcrumb of ancestor headings, outermost first, not including the chunk's own heading. Prepend it to the chunk text before embedding — retrieval quality improves when each chunk carries its place in the document.
markdownstring
The chunk's markdown, including its heading line. Reflects any stripLinks / stripImages flags you set.
wordCountinteger
Word count of the chunk.
approxTokensinteger
Rough token estimate (characters / 4) for budgeting embedding calls. Approximate by design.

Example

curl -X POST https://skim402.com/api/v1/read \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{ "url": "https://example.com/long-article", "chunked": true }'

The regular markdown, text, and metadata fields are unchanged — chunks is purely additive, and omitted entirely when the flag is off.

POST /v1/extract

Takes a URL plus a JSON Schema and returns schema-conforming JSON. Skim fetches and cleans the page the same way /v1/read does, then sends the markdown and your schema to an LLM (Anthropic Haiku 4.5 by default) via tool-use, which constrains the output to match your schema. No client-side parsing or validation — the returned data is guaranteed to conform.

The extraction runs only over the cleaned page content — not over the model's training data. If a field isn't on the page, the model leaves it out (or null) instead of filling in what it "knows." That makes the JSON trustworthy as a faithful read of the URL you pointed at, which is what you want when you're populating a CRM, building a tracker, or feeding a RAG index — the source of every value is the page itself.

Priced at $0.015 per call (roughly 7.5x basic read) because each request pays both the fetch and the extraction model. Failures (bad URL, no readable content, model refusal) return 4xx and the x402 facilitator skips settlement — you don't pay for an empty extract.

Request body

urlstring
required
The URL to fetch. Same http(s):// and SSRF rules as /v1/read.
schemaobject
required
A JSON Schema object describing the output shape. Must have type: "object" at the top level. Nested types, enums, required fields, and descriptions on properties are all respected by the model.
instructionsstring
Optional natural-language hint (e.g. "Only quote dollar figures from the article body, not the footer"). The schema is the primary contract — this is advisory.

Example

curl -X POST https://skim402.com/api/v1/extract \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{
    "url": "https://www.theverge.com/2024/anthropic-funding",
    "schema": {
      "type": "object",
      "properties": {
        "company":      { "type": "string" },
        "amount_usd":   { "type": "number" },
        "round":        { "type": "string", "enum": ["seed","series-a","series-b","series-c","series-d","series-e","other"] },
        "lead_investor":{ "type": "string" },
        "announced_on": { "type": "string", "format": "date" }
      },
      "required": ["company", "amount_usd"]
    }
  }'

Response

{
  "url": "https://www.theverge.com/2024/anthropic-funding",
  "finalUrl": "https://www.theverge.com/2024/anthropic-funding",
  "data": {
    "company": "Anthropic",
    "amount_usd": 4000000000,
    "round": "other",
    "lead_investor": "Amazon",
    "announced_on": "2024-03-27"
  },
  "schema": { "...": "echo of the schema you sent" },
  "source": {
    "metadata": { "title": "...", "byline": "...", "publishedAt": "..." },
    "readerCacheHit": false,
    "markdownBytes": 8142,
    "truncated": false
  },
  "model": "claude-haiku-4-5",
  "fetchedAt": "2026-05-22T14:11:03Z"
}

When to use it (and when not to)

Use /v1/extract when your pipeline needs typed data, not prose — populating a CRM, building a tracker, indexing for RAG with structured filters. If you just need clean markdown for an LLM to read inline, use /v1/read — it's 7.5x cheaper and gives the LLM the full article. Markdown longer than 20,000 characters is truncated before extraction; the source.truncated flag in the response tells you when that happened.

Extraction presets

Six built-in schemas for the most common extraction shapes, so your agent doesn't have to write a JSON Schema at all. Same pipeline, price ($0.015), and no-pay-on-failure contract as /v1/extract — the only difference is that the schema is baked in and echoed back in the response's schema field.

/v1/extract/articlePOST
Title, author, published date, summary, key points, language.
/v1/extract/productPOST
Name, brand, current price, currency, availability, rating, review count.
/v1/extract/jobPOST
Title, company, location, remote flag, employment type, salary bounds (only if stated), requirements.
/v1/extract/reviewPOST
Item name, rating and scale, author, verdict, pros, cons.
/v1/extract/eventPOST
Name, start/end dates, venue, city, country, organizer, ticket price.
/v1/extract/tablePOST
Every data table on the page as headers + rows — CSV-ready. Layout tables are skipped.

Request body

urlstring
required
The URL to fetch. Same http(s):// and SSRF rules as /v1/read.
instructionsstring
Optional hint to bias the extraction within the preset's schema (e.g. "prices are in the comparison table, not the hero banner"). Advisory only.

Example

curl -X POST https://skim402.com/api/v1/extract/product \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{ "url": "https://example.com/products/field-notebook" }'

Response

{
  "url": "https://example.com/products/field-notebook",
  "finalUrl": "https://example.com/products/field-notebook",
  "data": {
    "name": "Field Notebook, A5 Dot Grid",
    "brand": "Example Paper Co",
    "price": 14.5,
    "currency": "USD",
    "availability": "in stock",
    "rating": 4.7,
    "reviewCount": 312,
    "description": "A5 notebook with 120gsm dot-grid paper."
  },
  "schema": { "...": "the preset's JSON Schema, echoed" },
  "source": {
    "metadata": { "title": "...", "siteName": "..." },
    "readerCacheHit": false,
    "markdownBytes": 6210,
    "truncated": false
  },
  "model": "claude-haiku-4-5",
  "fetchedAt": "2026-07-13T10:20:00Z"
}

Use a preset when your target fits one of the six shapes; drop down to /v1/extract with your own schema when it doesn't. Values come only from the page — fields the page doesn't state come back null or omitted, never invented.

POST /v1/tables

Deterministic table extraction: every genuine HTML table on a page, parsed mechanically into structured JSON — no LLM involved. $0.003 per page, one-fifth the price of the AI-powered /v1/extract/table preset. Don't pay LLM rates for non-LLM work: if the data is already in <table> markup, this endpoint gets it out faster, cheaper, and with zero hallucination risk.

Each table comes back four ways at once: a headers array, raw rows (arrays of cell strings), records (objects keyed by header, when headers are detected and unique), and a GFM markdown rendering ready to paste into an LLM context. Merged cells (colspan/rowspan) are expanded so every row has the same width; layout tables (role="presentation", single-column, or wrapper tables around nested ones) are skipped.

Request body

urlstring
required
The URL to fetch. Same http(s):// and SSRF rules as /v1/read. PDFs are rejected with 415.

Example

curl -X POST https://skim402.com/api/v1/tables \
  -H "Content-Type: application/json" \
  -H "X-PAYMENT: <base64-payment-payload>" \
  -d '{ "url": "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)" }'

Response

{
  "url": "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)",
  "finalUrl": "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)",
  "tableCount": 6,
  "tables": [
    {
      "index": 0,
      "caption": "GDP forecast or estimate (million US$) by country",
      "headers": ["Country/Territory", "IMF Forecast", "World Bank Estimate"],
      "rows": [
        ["United States", "30,507,217", "29,184,890"],
        ["China", "19,231,705", "18,743,803"]
      ],
      "records": [
        { "Country/Territory": "United States", "IMF Forecast": "30,507,217", "World Bank Estimate": "29,184,890" }
      ],
      "rowCount": 210,
      "columnCount": 3,
      "markdown": "| Country/Territory | IMF Forecast | ..."
    }
  ],
  "fetchedAt": "2026-07-13T10:20:00Z"
}

If the page has no data tables at all, you get a 422 and no payment is taken — same no-pay-on-failure contract as every Skim endpoint. That includes pages whose tables are rendered by JavaScript or laid out as styled <div>s; for those, use /v1/extract/table — the LLM-powered preset reads tables no matter how they're built. Also available as GET /api/v2/tables?url=… for x402 v2 clients. Caps: 20 tables per page, 1,000 rows and 100 columns per table; a table that hits a cap carries "truncated": true so you know the output was clipped.

POST /v2/dataset

You define the columns. There are no fixed schemas or presets here — you hand Skim your own list of fields (up to 12, each with a name, a plain-language description, and a type), point it at one or more seed pages, and get back structured rows shaped exactly the way you asked, each with a per-row sourceUrl citation. Skim reads the seed pages, follows relevant links when the rows live on detail pages, extracts with the same constrained LLM as /v1/extract, and dedupes the result. $0.35 per build.

This is the endpoint for bespoke research pulls — competitive intelligence, market maps, lead lists. Want a table of competitor, pricingTier, price, featureSet, and sourceUrl across a set of pages? Define those five fields and that is the shape you get back, with every cell traceable to the page it came from.

Payment settles when the build is accepted (a 202 ticket), not when it finishes. Skim protects that contract two ways: your input is fully validated first, and the first seed URL is fetched before payment settles — if the seed page can't be read, you get a 4xx and no payment is taken. Builds typically finish in well under two minutes.

Request body

promptstring
required
What the dataset is — 10 to 500 characters. E.g. "Current YC companies in the AI batch with their one-line pitch".
fieldsarray
required
1–12 column definitions: { name, description, type? }. Names must be identifier-style (letters, digits, underscores); type is string (default), number, or boolean. The name sourceUrl is reserved — Skim adds it to every row automatically.
urlsarray
required
1–10 seed URLs to start from. Same http(s):// and SSRF rules as /v1/read.
rowLimitinteger
Maximum rows to return, 1–100. Default 50. A cap, not a promise — you get what the sources actually contain.
continueFromstring
A datasetId from an earlier build whose poll response said hasMore: true. Starts a new paid build that resumes exactly where that one stopped — inherits its prompt and fields (do not resend them), never repeats rows you already have. Optional rowLimit still applies.

Flow

The paid POST returns a 202 ticket immediately; the build runs in the background. Poll the ticket's pollUrl — polling is free, no payment header needed. The datasetId is the only key to the result, so treat it like a bearer secret.

curl -X POST https://skim402.com/api/v2/dataset \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: <base64-payment-payload>" \
  -d '{
    "prompt": "Top stories on the Hacker News front page",
    "fields": [
      { "name": "title", "description": "Story title" },
      { "name": "points", "description": "Upvote score", "type": "number" }
    ],
    "urls": ["https://news.ycombinator.com/"],
    "rowLimit": 25
  }'

# -> 202
{
  "datasetId": "ds_LviVNQtobY0pSEfxpRqB7K0O",
  "status": "running",
  "pollUrl": "/api/v2/dataset/ds_LviVNQtobY0pSEfxpRqB7K0O",
  "estimatedSeconds": 60
}

# then poll (free):
curl https://skim402.com/api/v2/dataset/ds_LviVNQtobY0pSEfxpRqB7K0O

Poll response (complete)

{
  "datasetId": "ds_LviVNQtobY0pSEfxpRqB7K0O",
  "status": "ready",
  "rowCount": 25,
  "rows": [
    {
      "title": "Tiny Emulators",
      "points": 207,
      "sourceUrl": "https://news.ycombinator.com/"
    }
  ],
  "stats": {
    "seedPages": 1,
    "followedPages": 12,
    "pagesFailed": 0,
    "llmCalls": 13,
    "durationMs": 48210
  },
  "hasMore": true,
  "continuation": {
    "continueFrom": "ds_LviVNQtobY0pSEfxpRqB7K0O",
    "pendingPages": 9,
    "bufferedRows": 4
  },
  "completedAt": "2026-07-13T10:21:00Z"
}

status is running, ready, partial (some source pages failed or the page budget ran out before full coverage — rows are still returned, with the details in stats), or error (no rows could be built; error says why). Values come only from the pages Skim read — every row cites the page it came from, and fields a source doesn't state come back null, never invented.

Continuation (pagination)

A single build is bounded — by your rowLimit and by an internal page budget — so large sources may not be fully drained in one pass. Nothing is lost silently: hasMore: true on the poll response means Skim is holding unread pages and/or already-extracted rows past your cap (pendingPages and bufferedRows in continuation tell you which). To get the next batch, POST a new paid build with just the token:

curl -X POST https://skim402.com/api/v2/dataset \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: <base64-payment-payload>" \
  -d '{ "continueFrom": "ds_LviVNQtobY0pSEfxpRqB7K0O" }'

The continuation inherits the original prompt and fields, starts from any buffered rows (no extra page cost), reads the unread frontier, and never repeats a row an earlier build in the chain already returned. Chain continueFrom through each new datasetId until hasMore comes back false. Each continuation is a regular $0.35 build with the same honesty contract: an invalid or fully-drained token is rejected before payment settles.

Custom URL watches

Watch the pages you care about. Register a list of 1 to 20 URLs once (POST /v2/watch, $0.01), then poll on your own schedule (GET /v2/watch/diff?id=, $0.005 per poll). Each poll re-reads every URL and answers one question: what changed since your last check? Diffs are computed against the clean extracted text, so a rotated ad or a new tracking script never shows up as a change — only content does.

The watch id is a private capability token — anyone who has it can poll (and pay for) your watch, so treat it like a bearer secret. The first poll baselines each page; changes are reported from the second poll on. Polls within 60 seconds of the previous one return the cached result with "fresh": true instead of re-reading.

Register

curl -X POST https://skim402.com/api/v2/watch \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: <base64-payment-payload>" \
  -d '{
    "urls": [
      "https://competitor.com/pricing",
      "https://competitor.com/changelog"
    ],
    "note": "competitor pricing + changelog"
  }'

# -> 201
{
  "watchId": "w_kF3nW8pQx2LmT9rYb6JcHd0V",
  "urls": ["https://competitor.com/pricing", "https://competitor.com/changelog"],
  "pollUrl": "/api/v2/watch/diff?id=w_kF3nW8pQx2LmT9rYb6JcHd0V",
  "minIntervalSeconds": 60
}

Input is validated and the first URL is fetched before payment settles — an unreadable first URL or a bad list means a 4xx and no charge, same contract as the dataset builder.

Poll for changes

curl "https://skim402.com/api/v2/watch/diff?id=w_kF3nW8pQx2LmT9rYb6JcHd0V" \
  -H "PAYMENT-SIGNATURE: <base64-payment-payload>"

# -> 200
{
  "watchId": "w_kF3nW8pQx2LmT9rYb6JcHd0V",
  "polledAt": "2026-07-14T12:00:00Z",
  "changedCount": 1,
  "urls": [
    {
      "url": "https://competitor.com/pricing",
      "status": "changed",
      "title": "Pricing — Competitor",
      "diff": {
        "addedCount": 1,
        "removedCount": 1,
        "changeRatio": 0.021,
        "addedSample": ["Pro plan — $49/month"],
        "removedSample": ["Pro plan — $39/month"],
        "numericOnly": true,
        "titleChanged": false
      }
    },
    {
      "url": "https://competitor.com/changelog",
      "status": "unchanged",
      "title": "Changelog — Competitor"
    }
  ]
}

Per-URL status is changed, unchanged, first_check (baseline stored on this poll), or error (the page could not be read this time; the old baseline is kept). For changed pages you get up to five added and five removed line samples, a changeRatio (share of lines that moved), and numericOnly true when every changed line differs only in digits, which lets an agent tell a price tick or view counter from a real content change without an LLM call.

GET /v2/watch/status?id= is free: the registered URLs, note, poll count, and last-poll time — no diff, no payment header needed.

GET /v2/feeds/x402/latest

The x402 ecosystem feed: new services and price changes across the public x402 registries, as structured items your agent can act on. $0.005 per poll. The feed refreshes on a 15-minute cycle — the first poll past that window triggers a fresh crawl of the registries; every poll inside it is served from storage. If a registry is down, the last good data is served instead (see crawl.status below).

This is a v2 endpoint (a plain GET with query parameters) — any x402 v2 client handles the payment handshake automatically.

Query parameters

limitinteger
Maximum number of items to return. Default 50, capped at 100. Items are newest-first.

Example

import { privateKeyToAccount } from "viem/accounts";
import { wrapFetchWithPayment } from "@x402/fetch";
import { x402Client } from "@x402/core/client";
import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";

const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(toClientEvmSigner(account)));
const paidFetch = wrapFetchWithPayment(fetch, client);

const res = await paidFetch(
  "https://skim402.com/api/v2/feeds/x402/latest?limit=25"
);
const feed = await res.json();

Response

{
  "feed": "x402",
  "asOf": "2026-07-13T12:00:00Z",
  "ttlSeconds": 900,
  "crawl": { "status": "ok", "lastCrawlAt": "2026-07-13T12:00:00Z", "durationMs": 2100 },
  "count": 2,
  "items": [
    {
      "id": 42,
      "kind": "new_service",
      "title": "New x402 service: Example API",
      "summary": "Example API appeared on x402-list.com (category: Data), priced from $0.005.",
      "source": "x402list",
      "url": "https://example-api.com",
      "payload": { "serviceKey": "example-api", "name": "Example API", "priceUsd": "0.005", "category": "Data" },
      "at": "2026-07-13T11:45:00Z"
    },
    {
      "id": 41,
      "kind": "price_change",
      "title": "Price change: Example API",
      "summary": "Example API on x402-list.com moved from $0.01 to $0.005.",
      "source": "x402list",
      "url": "https://example-api.com",
      "payload": { "serviceKey": "example-api", "oldPriceUsd": "0.01", "newPriceUsd": "0.005" },
      "at": "2026-07-12T09:30:00Z"
    }
  ]
}

Two item kinds today: new_service (a service appeared on a registry) and price_change (a listed price moved). at is when Skim first observed the change; where the registry publishes its own registration date, it is preserved in payload.registeredAt. If an upstream registry is down, you still get the stored items — crawl.status reports the last crawl attempt (ok, partial, or error) instead of the request failing. A paid poll never errors just because a registry hiccuped.

The Skim Signal series

Fifteen vertical intelligence feeds, one pattern: GET /v2/signal/<slug>/latest $0.005 per poll, refreshed on a 10-minute cycle, with the same stale-serve guarantee as the ecosystem feed: if an upstream source is down you still get the last good items, and crawl.status reports ok, partial, or error. Plain-language descriptions of every feed live on the Signals page.

Items carry a title, a short excerpt, the source, entity tags, a timestamp, and a link — the signal, not the full text. When your agent wants the whole page behind an item, that is one Skim /read call away.

Query parameters

limitinteger
Maximum number of items to return. Default 50, capped at 100. Items are newest-first.

A few feeds accept extra filters — see the table and the filter details below.

The fifteen feeds

SignalSlugSourcesFilters
AI/tech newsai-newsHacker News (score-gated), arXiv, vendor announcements
SEC filings radarsec-filingsSEC EDGAR, with a plain-language meaning tag per formforms=
Crypto newscrypto-newsCoinDesk, Cointelegraph, Decrypt, The Block; asset tags (BTC, ETH, ...)
Macro & regulatorsmacroFederal Reserve and SEC press releases
Security advisoriessecurityCISA advisories, CVE ids extracted as entities
US regulationsregulationsFederal Register: rules, proposed rules, presidential documents
US courtscourtsCourtListener: SCOTUS + 13 federal appellate courts, published opinions
Product recallsrecallsCPSC and FDA recalls, hazard/product tags
DealsdealsSlickdeals frontpage, DealNews; price/retailer/category tagscategories=
Product launcheslaunchesProduct Hunt: name, tagline, maker
Research papersresearcharXiv across 8 fields; abstract excerpts, authors, PDF linksfields=
EnergyenergyEIA Today in Energy + press room; commodity tags
EntertainmententertainmentDeadline, Variety, THR, TheWrap; studio/streamer/guild tags
Campaign financecampaign-financeFEC electronic filings; plain-language meaning per form typeforms=, committees=

Example

const res = await paidFetch(
  "https://skim402.com/api/v2/signal/ai-news/latest?limit=25"
);
const feed = await res.json();

Response items

{
  "feed": "ai-news",
  "asOf": "2026-07-13T12:00:00Z",
  "crawl": { "status": "ok", "lastCrawlAt": "2026-07-13T12:00:00Z" },
  "items": [
    {
      "kind": "story",
      "title": "New open-weights model tops reasoning benchmarks",
      "summary": "Hacker News front page: 412 points, 187 comments.",
      "source": "hackernews",
      "url": "https://example.com/model-announcement",
      "payload": { "score": 412, "comments": 187 },
      "at": "2026-07-13T11:40:00Z"
    }
  ]
}

The kind, source, and payload fields vary per feed — SEC items carry the form type and its plain-language meaning, deal items carry price and retailer, paper items carry authors and an abstract excerpt, and so on. The envelope is identical across all fifteen. (The sixteenth feed in the Signal series is the x402 ecosystem feed, documented above — it lives at its own path.)

Filter details

forms (sec-filings)string
Optional comma-separated filter. Tracked forms: 8-K, S-1, 10-K, 10-Q, SC 13D, SC 13G, 4.
categories (deals)string
Optional comma-separated category terms (1-8, each 2-40 characters), matched case-insensitively as substrings and OR-combined, e.g. categories=laptop,gpu.
fields (research)string
Optional comma-separated arXiv field filter. Tracked fields: ai, ml, nlp, vision, robotics, agents, security, quantum.
forms (campaign-finance)string
Optional comma-separated FEC form-type filter, e.g. forms=F3X,F24. Tracked forms: F1, F2, F3, F3P, F3X, F3L, F24, F5, F6, F9, F13, F99 — each item carries a plain-language meaning tag. Amendments and terminations collapse onto their base form.
committees (campaign-finance)string
Optional comma-separated committee-name terms (1-8, each 2-60 characters), matched case-insensitively as substrings and OR-combined, e.g. committees=turning point,dnc.

Reader modes

basic
$0.002

Static fetch + Readability. Fast, cheap, perfect for blogs, docs, and articles.

js
$0.005

Full headless-browser render for SPAs and JS-gated content. Use the separate /v1/read/js endpoint.

extract
$0.015

Pass a JSON Schema, get back typed JSON. Powered by an LLM constrained to your schema. See the separate /v1/extract endpoint.

Errors

Every error tells you two things: what went wrong, and whether your wallet got charged. Skim only settles payment on 2xx. If you didn't get markdown back, you didn't pay.

This isn't a refund mechanism. It's how x402 works. The facilitator settles after the route handler returns. If the handler returns an error, settlement is skipped. Failed reads cost zero.

400bad request
Malformed body, missing required field, or invalid URL. Fix the request before retrying. Wallet not charged.
402payment required
First-call response. Body contains the x402 payment requirement. Sign it with your client, retry with X-PAYMENT set. Wallet not charged — this is the prompt to pay, not a failed payment.
403forbidden
Target URL resolves to a blocked address (private, loopback, link-local, AWS metadata). SSRF protection. Wallet not charged.
422unprocessable
Fetched the page but couldn't extract readable content. Usually a JS-only SPA, an empty body, or aggressive paywall/bot-block. Try /v1/read/js for SPAs. Wallet not charged.
5xxserver error
Transient on Skim's side. Retry with exponential backoff (1s, 2s, 4s, give up after ~30s). Wallet not charged.

The pattern: only 2xx costs USDC. Everything else is free to hit, free to retry, free to throw away. Build your agent loop accordingly.