{
  "markdown": "# Region Fetch\n\n[![CI](https://github.com/regionfetch/regionfetch/actions/workflows/ci.yml/badge.svg)](https://github.com/regionfetch/regionfetch/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Node](https://img.shields.io/badge/node-%3E%3D20.11-brightgreen.svg)](https://nodejs.org)\n\n> **Pre-release.** Neither package is published to npm yet, so the install\n> commands below do not work today — build from source (see\n> [Development](#development)). The hosted API at `regionfetch.dev` is live.\n\nFetch a public HTTPS URL as a visitor in a specific country would see it, and get\na signed receipt proving where the retrieval actually came from.\n\nOne request. One x402 payment. One Ed25519-signed attestation of the exit\ncountry, supplier, final URL, and content hash.\n\nThis repository holds the public client packages:\n\n| Package | npm | What it is |\n| --- | --- | --- |\n| [`regionfetch`](packages/sdk) | `npm i regionfetch` | TypeScript SDK — fetch, recover, verify |\n| [`regionfetch-mcp`](packages/mcp) | `npx regionfetch-mcp` | MCP server exposing one `regionfetch` tool |\n\nThe service itself runs at **https://regionfetch.dev**.\n\n---\n\n## Quick start\n\n```bash\nnpm install regionfetch\n```\n\n```ts\nimport { RegionFetchClient } from \"regionfetch\";\n\nconst client = new RegionFetchClient();\n\nconst result = await client.fetchUrl(\n  { url: \"https://example.com/pricing\", country: \"DE\", mode: \"browser\" },\n  { paymentSignature },\n);\n\nconsole.log(result.data.body);\nconsole.log(result.receipt.attestation.payload.country); // \"DE\"\n```\n\n### Supported countries and modes\n\nCountries: **US, DE, JP, BR, IN**. Anything else is rejected locally before a\npayment is created.\n\n| Mode | What it does |\n| --- | --- |\n| `http` (default) | Direct proxied GET with a country-appropriate locale, user agent, and `Accept-Language`. Follows up to five redirects, revalidating each target. |\n| `browser` | Headless Chromium through the regional proxy, with a country-appropriate locale, timezone, viewport, and user agent. Waits for `domcontentloaded` and returns rendered HTML. |\n\n### Escalation tiers\n\n`max_tier` caps how hard the service will work to retrieve a blocked page:\n`L0` and `L1` use the standard regional supplier, `L2` permits escalation to a\nmanaged unblocker and costs more. Defaults to `L1`.\n\nThe client never assumes a price — it authorizes exactly the amount the\ndeployment returns in its challenge, so a dearer tier needs no client change.\n`L3` is not a supported capability and is rejected before any payment is\ncreated.\n\nL2 is priced higher than L0/L1 — $0.05 against $0.02 at the time of writing —\nand the price comes from the challenge, never from the client.\n\n### Pricing\n\nThe default list price is **$0.02 USDC per request** on **Base**\n(`eip155:8453`). Price, network, timeout, and response-size limits are\ndeployment-controlled — read them from the 402 challenge rather than hardcoding\nthem.\n\n---\n\n## Paying\n\n### Use the scoped `@x402/*` packages, not `x402` or `x402-fetch`\n\nThis matters more than it looks. The deployment speaks **x402 protocol v2**:\n\n- CAIP-2 networks (`eip155:8453`), not chain names (`base`)\n- an `amount` field, not `maxAmountRequired`\n- the challenge in a **`PAYMENT-REQUIRED` response header**, with an empty body\n- a **`PAYMENT-SIGNATURE`** request header, not `X-PAYMENT`\n\nThe unscoped `x402` and `x402-fetch` packages on npm are **v1 only**. They parse\nthe challenge from the response body, hardcode `x402Versions = [1]`, reject a\nCAIP-2 network at the schema, and send `X-PAYMENT`. They cannot pay this API.\n\nInstall the v2 stack:\n\n```bash\nnpm install regionfetch @x402/core @x402/evm viem\n```\n\n### Wallet mode — a fresh authorization per request\n\n```ts\nimport { RegionFetchClient } from \"regionfetch\";\nimport { createX402PaymentProvider } from \"regionfetch/x402\";\n\nconst paymentProvider = await createX402PaymentProvider({\n  privateKey: process.env.WALLET_KEY,\n  maxAmountPerPayment: \"$0.10\", // per-payment ceiling; default is $0.10\n});\n\nconst client = new RegionFetchClient({ paymentProvider });\nconst result = await client.fetchUrl({ url: \"https://example.com/\", country: \"JP\" });\n```\n\nThe SDK sends one unpaid probe, reads the challenge from the header, asks the\nprovider for exactly one authorization, and retries once. It will never mint a\nsecond payment for one logical fetch — if the paid attempt is still refused, you\nget an error rather than a second charge.\n\n### Explicit mode — you already hold an authorization\n\n```ts\nawait client.fetchUrl(input, { paymentSignature });\n```\n\nOne authorization funds one durable request. Reusing it for a *different* body\nis rejected. Resending the *identical* body with the *identical* authorization\nafter a dropped connection is the correct recovery — see below.\n\n### Bring your own wallet\n\n`paymentProvider` is a one-method interface, so you are not tied to viem or to\nany particular custody model:\n\n```ts\nconst paymentProvider = {\n  async createPayment({ paymentRequired, resourceUrl }) {\n    return myWalletService.authorize(paymentRequired, resourceUrl);\n  },\n};\n```\n\n---\n\n## Verifying receipts\n\nEvery terminal outcome — success *and* paid failure — carries a signed receipt.\n\n```ts\nimport { verifyReceipt } from \"regionfetch\";\n\nconst key = await client.getAttestationKey();\n\nconst verification = verifyReceipt(result.receipt, {\n  expectedKeyId: key.keyId,\n  expectedPublicKeyPem: key.publicKeyPem,\n  responseBody: result.data.body,\n});\n\nverification.valid; // true only if every check that ran passed\n```\n\nFour independent checks, because a valid signature alone proves less than it\nappears to:\n\n| Check | What it rules out |\n| --- | --- |\n| `signatureValid` | Tampering with the signed bytes |\n| `payloadMatches` | A `payload` edited after signing, leaving `payloadCanonical` intact |\n| `receiptFieldsMatch` | Convenience fields at the receipt root disagreeing with what was signed |\n| `bodyHashMatches` | A body swapped in transit |\n| `keyMatchesExpectation` | A self-consistent receipt minted with someone else's key |\n\nTwo things the implementation is strict about:\n\n- **Verification uses `payloadCanonical` verbatim.** Re-serializing\n  `attestation.payload` can produce different bytes and either fails spuriously\n  or, worse, masks a mismatch.\n- **The key's real type is checked, not its label.** `attestation.algorithm` is\n  attacker-controlled text; an RSA key presented as `\"Ed25519\"` is rejected.\n\nWithout a pinned or independently retrieved key, verification proves internal\nconsistency only — anyone can mint a self-consistent receipt with their own key.\nPass `expectedKeyId` / `expectedPublicKeyPem` when you need origin trust.\n\n---\n\n## Errors and recovery\n\n```ts\nimport { RegionFetchApiError } from \"regionfetch\";\n\ntry {\n  await client.fetchUrl(input, { paymentSignature });\n} catch (error) {\n  if (error instanceof RegionFetchApiError && error.isPaidFailure) {\n    // Payment settled; the attempt failed terminally. Keep the receipt —\n    // it is the evidence that the paid attempt happened.\n    await store(error.receipt);\n  }\n}\n```\n\n| Status | Meaning | What the SDK does |\n| --- | --- | --- |\n| `400` | Invalid body, country, mode, or target | Throws. Do not retry unchanged. |\n| `402` | Payment absent or unverified | Throws `RegionFetchPaymentRequiredError` with the decoded challenge |\n| `409` | Same payment already processing | Retries the identical request with the same authorization |\n| `429` | Rate limited | Retries with exponential backoff, honouring `Retry-After` |\n| `502` / `504` | Terminal paid failure | Throws with the signed receipt attached. Never retried. |\n| `503` | Service or dependency unavailable | Throws. Not retried by default. |\n\nDefault policy: two retries, exponential backoff with jitter, `Retry-After`\nrespected. Configure with `retry: { maxRetries, baseDelayMs, maxDelayMs }`.\n\n### After an ambiguous failure\n\nIf the connection drops after you sent a payment, **do not create a second\npayment** — but be aware that resending the identical authorization will not\nrecover the result either. x402 `exact` uses EIP-3009, whose nonce is spent\non-chain at settlement, so a settled payment is refused with `402` rather than\nreplayed. Resending is harmless (it cannot charge twice), it just will not work.\n\nThe reliable recovery is the status endpoint, so **capture `requestId` from\nevery response you do receive**:\n\n```ts\nconst state = await client.getRequest(requestId);\n```\n\n| `state.status` | Meaning |\n| --- | --- |\n| `succeeded` / `failed` | Terminal. A stored failure arrives as HTTP 200, with the detail in the receipt. |\n| `settling` / `executing` | Still in flight. |\n| `unresolved` | Execution may have happened but was never persisted. The server will not re-run it — that could duplicate an external effect after payment. Reconcile against the settlement transaction. |\n\n---\n\n## Local validation is load-bearing\n\nThe deployment's payment gate runs **ahead of** body validation: an unpaid\nrequest with `country: \"ZZ\"` still answers 402, not 400. So the SDK validates\ninput before touching the network — otherwise you would mint an authorization\nfor a request the server then rejects.\n\nChecked locally: HTTPS-only, URL length ≤ 2048, known country, known mode, no\nunknown fields, and obvious loopback/private targets. The server remains\nauthoritative for DNS resolution and the real public-target policy.\n\n---\n\n## MCP server\n\n```bash\nnpx -y regionfetch-mcp\n```\n\n```json\n{\n  \"mcpServers\": {\n    \"regionfetch\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"regionfetch-mcp\"],\n      \"env\": {\n        \"REGION_FETCH_BASE_URL\": \"https://regionfetch.dev\",\n        \"REGION_FETCH_WALLET_PRIVATE_KEY\": \"0x...\",\n        \"REGION_FETCH_MAX_PAYMENT\": \"$0.10\"\n      }\n    }\n  }\n}\n```\n\nExposes exactly one tool, `regionfetch`, taking `url`, `country`, and optional\n`mode`. Results use the `{ status, ok, response }` envelope, with the upstream\nJSON body preserved verbatim and `isError: true` on any non-2xx — including paid\nfailures, whose signed receipts survive intact.\n\nSee [packages/mcp/README.md](packages/mcp/README.md) for configuration,\npayment modes, and the full environment variable list.\n\n---\n\n## Repository layout\n\n```\npackages/sdk/     regionfetch      — client, verification, x402 provider\npackages/mcp/     regionfetch-mcp  — stdio MCP server\nexamples/                          — runnable integration examples\nopenapi/                           — client-side snapshot of the HTTP contract\ntest/                              — opt-in live compatibility smoke test\n```\n\n### Development\n\n```bash\nnpm install\nnpm run build\nnpm test\n```\n\nLive checks against production are gated and never run in CI:\n\n```bash\nREGION_FETCH_LIVE_SMOKE=1 npm run smoke                      # free\nREGION_FETCH_LIVE_SMOKE_SPEND=1 \\\n  REGION_FETCH_WALLET_PRIVATE_KEY=0x... npm run smoke        # spends USDC\n```\n\n---\n\n## Security\n\nNever pass a private key as a fetch argument or an MCP tool input. Payment\nsignatures are bearer credentials — the SDK redacts them from every diagnostic\npath and the MCP server keeps them off both stdout and stderr.\n\nSee [SECURITY.md](SECURITY.md).\n\n## Contract notes\n\n[`openapi/regionfetch.openapi.yaml`](openapi/regionfetch.openapi.yaml) is the\nSDK's snapshot of the HTTP contract, verified against the live API. An\nauthoritative document published by the service will supersede it.\n\n[CONTRACT.md](CONTRACT.md) records the behaviours the client is built around —\nincluding why the x402 challenge is read from a header rather than the response\nbody, and why input is validated before any payment is created.\n\n## Contributing\n\nIssues and pull requests are welcome. Please run `npm run build && npm test &&\nnpm run typecheck` before opening a PR, and add a test for any behaviour change\n— the contract tests in `packages/sdk/test` are the guard against silently\ndrifting away from the deployed API.\n\nFor anything security-sensitive, follow [SECURITY.md](SECURITY.md) rather than\nopening a public issue.\n\n## Versioning\n\nSemVer. A new country or mode is an additive minor; removing one, renaming a\nfield, or changing payment or receipt-signing semantics is a major. The receipt\npayload `version` (currently `\"1\"`) is versioned independently of the package.\n\n## License\n\nMIT\n",
  "bytes": 12283,
  "sha": "edfb25f832f0c2f91d08cfc7e4bdafff3280c772c4fb60dd326de7c5d0df3ecc",
  "repo_slug": "regionfetch/regionfetch",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_dev_regionfetch_mcp_f59b4b83/readme"
}