{"message":"Full documentation (README) as HTML.","url":"/readme","markdown":"# Pay with XPR on the Web\n\nThis site shows how to put a **paywall** on the web using **XPR** (Xprnetwork): visitors pay a small amount in XPR (or XUSDC) to unlock a page. No sign‑up forms or API keys—just pay and get access.\n\n**Live site:** [https://xpr.testsitout.com](https://xpr.testsitout.com)\n**Source code:** [https://github.com/French-River-Hosting/xpr-x402](https://github.com/French-River-Hosting/xpr-x402)\n\n---\n\n## What you get\n\n- **Free pages** — Home, an intro to the [x402](https://www.x402.org) payment standard, and live XPR chain info.\n- **One paywalled page** — “Premium.” To see it, you send a tiny payment in XPR (e.g. 0.1 XPR) to the site’s receiver account, then paste the **transaction id** to unlock.\n\nEverything runs on **Cloudflare Workers** and talks to **XPR Network (Proton)** to check that the payment really happened.\n\n---\n\n## Try it at home\n\nUse the live site and walk through the flow:\n\n1. **Open the protected page** — [https://xpr.testsitout.com/protected-route](https://xpr.testsitout.com/protected-route). You’ll see “Payment required” with the amount (e.g. **0.1000 XPR**) and the **receiver account** (e.g. **cbeau**).\n2. **Send that amount** from any XPR wallet (e.g. [Proton Wallet](https://www.protonchain.com/)) to that receiver account. In the memo you can put anything (e.g. `paywall`).\n3. **Copy the transaction id** from your wallet or from the [Proton explorer](https://proton.bloks.io).\n4. **Unlock the page** — either go back to the protected page and paste the transaction id in the form, or open: `https://xpr.testsitout.com/protected-route?tx_id=YOUR_TX_ID`\n\nIf the payment is valid, you’ll see the premium content. Each transaction id works only once.\n\n**If the site uses a facilitator:** the 402 response will include a **facilitatorUrl** (and in multi-tenant, **facilitatorAudience**). You can POST your `tx_id` (and `audience` if present) to the facilitator to get a short-lived **Bearer token**, then unlock with `Authorization: Bearer <token>`. The [samples doc](/docs/samples) on the site has a full copy-paste curl flow (including retry on 402 for indexer lag).\n\n**Full flow and testing:** For a step-by-step description of the facilitator flow (single-tenant and multi-tenant), setup, and how to test with PowerShell and curl, see **[Onboarding & facilitator flow](/docs/facilitator)**. For raw markdown use **[/readme?format=md](/readme?format=md)** and **[/docs/facilitator?format=md](/docs/facilitator?format=md)**.\n\n**For LLMs / reproducibility:** The site serves **[/llm.txt](https://xpr.testsitout.com/llm.txt)** and **[/llm.md](/llm.md)** with a concise project summary (stack, structure, env, flow, how to recreate) so agents or humans can reproduce this setup.\n\n---\n\n## How the flow works\n\n1. **You request the protected page** (in a browser or with `curl`). You don’t send any payment yet.\n2. **The server responds with 402 Payment Required** and a JSON body that includes:\n   - **payTo** — the XPR account that receives the payment (e.g. `cbeau`)\n   - **maxAmountRequired** — the amount to send (e.g. `0.1000 XPR` or `0.100000 XUSDC`)\n   - **resource**, **network**, **explorer** link, and short instructions.\n   - **facilitatorUrl** (optional) — if the facilitator Worker is deployed, URL to POST your tx id and get a Bearer token.\n3. **You send that amount** to **payTo** using any XPR wallet or the Proton CLI. You get a **transaction id**.\n4. **You request the protected page again** with proof of payment:\n   - **Direct:** `GET /protected-route?tx_id=YOUR_TX_ID` or header `X-XPR-Transaction-Id` / `X-Payment-Authorization: YOUR_TX_ID`. The server verifies the transaction on-chain (Hyperion) and marks it used.\n   - **Via facilitator (if configured):** POST `{ \"tx_id\": \"YOUR_TX_ID\", \"resource\": \"/protected-route\" }` to **facilitatorUrl**. You get a JWT. Then call the protected route with `Authorization: Bearer <token>`. The server trusts the facilitator and does not call the chain for that request.\n5. **The server** either verifies the transaction on XPR Network (direct flow) or verifies the facilitator’s JWT (facilitator flow), checks that the tx id hasn’t been used before, and returns **200** and the premium content.\n\nSo: **no account on the website**, no API key—only proof that you sent XPR (or XUSDC) to the right place.\n\n---\n\n## Sample requests\n\nFor a **client sample** (minimal curl/jq) and an **agentic sample** (steps for AI agents), see **[Client & agent samples](/docs/samples)** on the site (or `/docs/samples?format=md` for markdown).\n\n**See what the server wants (402):** Use `Accept: application/json` to get a JSON body (otherwise the server may return HTML).\n\n```bash\ncurl -sS -H \"Accept: application/json\" https://xpr.testsitout.com/protected-route\n```\n\nYou’ll get status **402** and a JSON body with `payTo`, `maxAmountRequired`, `asset`, `facilitatorUrl`, `facilitatorAudience` (if configured), etc.\n\n**Unlock after paying (200):**\n\n```bash\ncurl -sS -H \"Accept: application/json\" -H \"X-Payment-Authorization: YOUR_TX_ID\" https://xpr.testsitout.com/protected-route\n# or\ncurl -sS -H \"Accept: application/json\" -H \"Authorization: Payment tx_id=YOUR_TX_ID\" https://xpr.testsitout.com/protected-route\n# or (legacy/dev convenience)\ncurl -sS -H \"Accept: application/json\" \"https://xpr.testsitout.com/protected-route?tx_id=YOUR_TX_ID\"\n# If facilitator is deployed: POST tx_id to facilitatorUrl, then:\n# curl -sS -H \"Accept: application/json\" -H \"Authorization: Bearer YOUR_TOKEN\" https://xpr.testsitout.com/protected-route\n```\n\n**Free routes (no payment):**\n\n```bash\ncurl https://xpr.testsitout.com/\ncurl https://xpr.testsitout.com/x402\ncurl https://xpr.testsitout.com/xpr\n```\n\n---\n\n## Routes (reference)\n\n| Route | Description |\n|-------|-------------|\n| `GET /` | Home: intro, links, and how to test. |\n| `GET /ai` | Machine-discovery JSON index (canonical docs + route + payment capabilities). |\n| `GET /x402` | Short overview of the x402 payment standard and links to whitepaper and docs. |\n| `GET /xpr` | Live chain info from XPR Network (Proton) mainnet. |\n| `GET /protected-route` | **Paywalled.** Pay in XPR (or XUSDC) to the configured receiver, then pass `?tx_id=...` or header `X-XPR-Transaction-Id` to unlock. |\n| `GET /readme` | Full documentation (this README) published on the site. |\n| `GET /docs/facilitator` | Onboarding & facilitator flow (setup, testing). |\n| `GET /docs/samples` | Client & agent samples (curl, pseudocode). |\n| `GET /llm.txt`, `GET /llms.txt`, `GET /llm.md` | Project summary for LLMs (plain text alias + markdown). |\n| `GET /robots.txt` | Crawler and AI-agent discovery hints. |\n| `GET /sitemap.xml` | URL discovery for search engines and agents. |\n\n---\n\n## Configuration\n\nThe app uses **Cloudflare Workers** and reads config from **Wrangler** (e.g. `wrangler.toml`).\n\n| Variable | Meaning |\n|----------|---------|\n| `XPR_RECEIVER_ACCOUNT` | XPR account that receives payments (e.g. `cbeau`). |\n| `XPR_MIN_AMOUNT` | Minimum payment, e.g. `0.1000 XPR` or `0.100000 XUSDC`. |\n| `FACILITATOR_URL` | (Optional) Base URL of the facilitator Worker (e.g. `https://facilitator-xpr.testsitout.com`). If set, 402 responses include `facilitatorUrl` and the server accepts `Authorization: Bearer <facilitator-jwt>`. |\n| `FACILITATOR_JWT_SECRET` | (Optional) Secret used to verify JWTs issued by the facilitator. When using a **shared** facilitator (multi-tenant), this is the secret you got when registering your audience. |\n| `FACILITATOR_AUDIENCE` | (Optional) When using a shared facilitator, your audience id. The 402 will include `facilitatorAudience` so clients can POST it to the facilitator; your Worker will only accept JWTs with matching `aud`. |\n\nSet the first two in the `[vars]` section of `wrangler.toml`. For local dev you can override with `.dev.vars` (see `.dev.vars.example`). When you deploy the **facilitator** Worker, set `FACILITATOR_URL` and `FACILITATOR_JWT_SECRET` on the main Worker so the site advertises and accepts the facilitator flow.\n\n**JWT secret (facilitator):** Use a single strong secret for both the facilitator (to sign) and the main Worker (to verify). Never commit it.\n\n- **Generate:** e.g. `openssl rand -base64 32` or PowerShell: `[Convert]::ToBase64String((1..32 | % { Get-Random -Maximum 256 }))`.\n- **Production:** Set it as a **secret** in Cloudflare (not in `wrangler.toml`): from the project root run `npx wrangler secret put FACILITATOR_JWT_SECRET` (main Worker) and from `facilitator/` run `npx wrangler secret put FACILITATOR_JWT_SECRET` (facilitator Worker). Paste the same value both times.\n- **Local dev:** Put `FACILITATOR_JWT_SECRET=your-secret` in `.dev.vars` for both the main app and the facilitator (each has its own `.dev.vars` in its directory if you run both locally).\n\n- **XPR** is sent via the **eosio.token** contract.  \n- **XUSDC** is sent via the **xtokens** contract. The Worker checks the right contract based on `XPR_MIN_AMOUNT`.\n\n---\n\n## Deploy\n\n```bash\nnpm run dev        # local\nnpm run deploy     # deploy to Cloudflare\n```\n\nBefore the first deploy, create the KV namespace used for one‑time transaction ids (so each payment unlocks only once):\n\n```bash\nnpx wrangler kv:namespace create USED_TX_IDS\n```\n\nPut the returned id in `wrangler.toml` under `kv_namespaces` for `USED_TX_IDS`. Without KV, the app still runs but the same `tx_id` can be reused (fine for local testing).\n\nThe demo is set up for **https://xpr.testsitout.com**. Your Cloudflare account must have the zone (e.g. `testsitout.com`) and the route configured.\n\n**Publishing docs on the site:** Run `npm run build:docs` before deploying so that `/readme`, `/docs/facilitator`, `/docs/samples`, and `/llm.txt` serve current content.\n\n**Optional: Deploy the facilitator** so clients can get a Bearer token instead of sending the tx id. See **[/docs/facilitator](/docs/facilitator)** (or raw markdown at **[/docs/facilitator?format=md](/docs/facilitator?format=md)**). After deploying the facilitator, set `FACILITATOR_URL` and `FACILITATOR_JWT_SECRET` on the main Worker (e.g. in the Cloudflare dashboard).\n\n---\n\n## If others want to use this\n\n**Option A — Each person runs their own stack (recommended):** Others clone or fork the repo, deploy both the main Worker and the facilitator under their own Cloudflare account, and generate their own JWT secret. They set the same secret on both of their Workers. No secrets are shared; each site has its own receiver account, KV, and facilitator.\n\n**Option B — You run a shared facilitator; partners get their own secret (multi-tenant):** You deploy one facilitator and set `FACILITATOR_ADMIN_SECRET`. Partners **register** with you to get an **audience** id and a **secret** they can rotate:\n\n1. You (or they) call **POST /register** on your facilitator with header `X-Admin-Key: <your admin secret>` and body `{ \"audience\": \"their-site-id\", \"secret\": \"a-strong-secret-they-choose\" }`. The facilitator stores that secret in KV.\n2. They set on their main Worker: `FACILITATOR_URL` (your facilitator), `FACILITATOR_JWT_SECRET` (that secret), and `FACILITATOR_AUDIENCE` (their audience id). Their 402 responses will include `facilitatorAudience`; clients POST `tx_id` and `audience` to your facilitator and get a JWT signed with that partner’s secret.\n3. To **rotate** the secret, they (or you) call **PUT /keys/:audience** with `X-Admin-Key` and body `{ \"secret\": \"new-secret\" }`. They then update their Worker’s `FACILITATOR_JWT_SECRET`. Old tokens expire in 5 minutes.\n\nYour facilitator’s payment config (receiver, min amount) still applies to all; each partner just has their own signing key and can rotate it without affecting others.\n\n**Option C — Public template / tutorial:** Publish the repo (e.g. on GitHub) and document the deploy steps and JWT secret handling above. Others deploy their own Workers and secrets; no shared infrastructure.\n\n---\n\n## How payment is verified\n\n- **Direct flow (tx_id):** The Worker calls XPR Network’s **Hyperion** history API to load the transaction by id, checks that it includes a **transfer** of the configured token (XPR or XUSDC) **to** `XPR_RECEIVER_ACCOUNT` with amount **≥** `XPR_MIN_AMOUNT`, then stores the transaction id in **KV**.\n- **Facilitator flow (Bearer token):** The client POSTs the tx id to the **facilitator** Worker, which verifies the transaction on Hyperion and issues a short-lived JWT. The main Worker verifies the JWT (shared secret), extracts the tx id, and marks it used in KV. The main Worker does not call the chain when the client uses a valid facilitator token.\n- If the same tx id is used again (either way), the server returns 402 with `payment_already_used`.\n\n---\n\n## API / 402 response shape\n\nFor clients and scripts, the protected route follows [x402](https://www.x402.org)-style **402 Payment Required**:\n\n**When payment is required (402),** the JSON body includes:\n\n- `maxAmountRequired` — e.g. `\"0.1000 XPR\"`\n- `payTo` / `paymentAddress` — receiver account\n- `resource` — `\"/protected-route\"`\n- `description` — short instructions\n- `network` — `\"proton-mainnet\"`\n- `asset` / `currency` — `\"XPR\"` or `\"XUSDC\"`\n- `paymentId`, `expiresAt`, `accepts` (machine-readable payment options), `howToPay`, `explorer`\n- `facilitatorUrl` (optional) — when the facilitator is deployed, URL to POST the tx id and get a Bearer token (e.g. `https://facilitator.example.com/verify`)\n- Header `WWW-Authenticate: Payment ...` — x402/MPP-compatible challenge metadata (`id`, `method`, `intent`, `request`, `expires`)\n\n**Sending proof of payment** — after paying, retry with one of:\n\n- Header: `X-XPR-Transaction-Id: TRANSACTION_ID`\n- Header: `X-Payment-Authorization: TRANSACTION_ID`\n- Header: `Authorization: XPR tx_id=TRANSACTION_ID`\n- Header: `Authorization: Payment tx_id=TRANSACTION_ID` (MPP/x402 style)\n- Header: `Authorization: Payment <base64url-json-credential>` where JSON includes `tx_id` (compat mode)\n- Header: `Authorization: Bearer <facilitator-jwt>` (when facilitator is in use; get the JWT by POSTing `{ \"tx_id\", \"resource\" }` to `facilitatorUrl`)\n- Query (legacy/dev convenience): `?tx_id=TRANSACTION_ID`\n\n**After a valid payment (200),** the JSON includes `resource`, `paid: true`, `currency`, and a `message`, plus:\n\n- `Payment-Response` header (x402-style result metadata)\n- `Payment-Receipt` header (legacy compatibility envelope)\n\n## Protocol compatibility notes\n\n- **Core x402/MPP semantics:** `402 Payment Required`, `WWW-Authenticate: Payment ...`, `Authorization: Payment ...`, and `Payment-Response` on success.\n- **Project extensions:** direct tx-id proof (`X-XPR-Transaction-Id`, `X-Payment-Authorization`, `Authorization: XPR ...`) and facilitator JWT (`Authorization: Bearer ...`).\n- **Rail-specific behavior:** on-chain validation is XPR/Proton based (Hyperion lookup + token transfer checks), not EVM/USDC by default.\n\nFor a reusable server helper that mirrors an `mpp-xpr` style API (`createChallenge`, `buildWwwAuthenticateHeader`, `parseAuthorization`, `verify`, `buildPaymentReceiptHeader`), see `src/mpp-xpr-equivalent.ts`.\n\n---\n\n## Getting an XPR account and keys\n\n- **Use an existing wallet (e.g. Proton/WebAuth):** You need the **private key** for the account that will pay. In the Proton mobile app: **Settings → Backup wallet** (or similar); copy the private key (`PVT_K1_...`). Then run `proton key:add` and paste it when prompted.\n- **Create a new account from the CLI:** Run `proton chain:set proton` and `proton account:create myname`. Follow the prompts (email, etc.). Save the private key the CLI shows. Fund the new account with a little XPR from another wallet, then use it to pay the protected route as above.\n\n---\n\n## References\n\n- [x402](https://www.x402.org) — Open payment standard for HTTP 402; [whitepaper](https://www.x402.org/x402-whitepaper.pdf), [docs](https://docs.x402.org).\n- [Xprnetwork docs](https://docs.xprnetwork.org) — Chain, endpoints, SDKs, signing.\n- [Source code](https://github.com/French-River-Hosting/xpr-x402)\n\nThis app uses **x402-style** 402 payloads with **XPR on Proton** (proof by transaction id). The broader x402 ecosystem also includes USDC on EVM and signed authorizations.\n"}