From 8d06d2d443d9ef2e8e2e4c257a34929ab4725201 Mon Sep 17 00:00:00 2001 From: fretchen Date: Wed, 11 Nov 2026 07:49:17 +0100 Subject: [PATCH 1/8] searchapi: derive the 402 resource.url from the committed spec, drop SEARCH_SERVICE_URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A leftover trial-deploy env value advertised the *.bunny.run origin as the payment scope on the direct routes, making #97600 unattributable to web-agent.fretchen.eu for indexers (cdp-sdk#835). resource.url is the payment scope, not the serving origin, so it now always names the production identity from openapi.search.json's servers[0].url — the same pattern as the llm twin. The stale env var was removed from script 97472 (verified live: search and fetch both quote https://web-agent.fretchen.eu now). Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/deploy-bunny-js.yml | 2 +- bunny_js/README.md | 12 +++++------- bunny_js/src/searchapi_routes.ts | 13 ++++++++----- bunny_js/test/routes.test.ts | 4 +++- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-bunny-js.yml b/.github/workflows/deploy-bunny-js.yml index 26882f1c3..207364f7c 100644 --- a/.github/workflows/deploy-bunny-js.yml +++ b/.github/workflows/deploy-bunny-js.yml @@ -8,7 +8,7 @@ name: Deploy Bunny JS twins # # This workflow ships CODE only. Each script's environment (BRAVE_API_KEY, # MISTRAL_API_KEY, the receiver authorizer key, the S3 channel-store credentials, RPC -# URLs, SEARCH_SERVICE_URL, the Cockpit observability pair SCW_COCKPIT_PUSH_TOKEN + +# URLs, the Cockpit observability pair SCW_COCKPIT_PUSH_TOKEN + # COCKPIT_LOKI_URL) is configured on the script itself — bunny dashboard or # `bunny scripts env set` — and is deliberately not part of CI: a repo leak must not be # a secrets leak. diff --git a/bunny_js/README.md b/bunny_js/README.md index 12eca3978..4037e19ca 100644 --- a/bunny_js/README.md +++ b/bunny_js/README.md @@ -130,7 +130,6 @@ it is inert without another credential, plain — and plain stays inspectable wh | `RPC_URL_EIP155_10`, `RPC_URL_EIP155_8453` | secret (`--secret`) | viem chain reads — NOT in `.env`: production runs unset on viem's public defaults (the accepted tradeoff in scw_js's serverless.yml). Set as `--secret` only if rate limits bite — real values embed an API key | | `FACILITATOR_URL` | plain (code default ok) | `https://facilitator.fretchen.eu` | | `LLM_WITHDRAW_DELAY_SECONDS` | plain (code default ok) | 86400 — must match production | -| `SEARCH_SERVICE_URL` | plain | advertised `resource.url` base; the bunny trial URL while trialing, production default otherwise | | `SCW_COCKPIT_PUSH_TOKEN` | secret (`--secret`) | Cockpit push-only token — authenticates the Loki log flush to the observability data source (see the observability README/implementation plan) | | `COCKPIT_LOKI_URL` | plain | push URL of the custom Cockpit logs data source — public and inert without the token | @@ -259,12 +258,11 @@ and the `.bunny/script.json` link comes out of the create step. One-time sequenc 3. `npm run deploy` (searchapi) / `bunny scripts deploy dist/llm.js ` / `bunny scripts deploy dist/mcp.js ` — publishes each; the output prints the live `*.bunny.run` URL. -4. `bunny scripts env set SEARCH_SERVICE_URL https://.bunny.run` on the searchapi - script and redeploy, so the 402 advertises the trial origin instead of the parity default. - (llm/mcp have no equivalent — their 402s quote the spec's committed server URL.) -5. Unpaid smoke: `curl -D - 'https://.bunny.run/search?q=x402'` → 402 + - `Payment-Required` header. -6. For the GitHub Action: `bunny scripts show` (or `list`) gives each script id; the deploy +4. Unpaid smoke: `curl -D - 'https://.bunny.run/search?q=x402'` → 402 + + `Payment-Required` header. The 402's `resource.url` keeps naming the production origin + (it is derived from the committed spec, like the llm twin) — the resource url is the + payment scope, not the serving origin, so a trial does not change it. +5. For the GitHub Action: `bunny scripts show` (or `list`) gives each script id; the deploy keys are in the dashboard (Script -> Deployments -> Settings). Add each pair as the `BUNNYJS_*` / `BUNNYLLM_*` / `BUNNYMCP_*` repo secrets. diff --git a/bunny_js/src/searchapi_routes.ts b/bunny_js/src/searchapi_routes.ts index 65fb88f44..4239910a6 100644 --- a/bunny_js/src/searchapi_routes.ts +++ b/bunny_js/src/searchapi_routes.ts @@ -65,12 +65,15 @@ const MAINNET_NETWORKS = ["eip155:10", "eip155:8453"]; const MAX_TIMEOUT_SECONDS = 30; /** - * Base for the advertised `resource.url` — the production custom domain by default, so the - * twin's published terms match the Scaleway original; set SEARCH_SERVICE_URL to the - * bunny-provided URL when deploying the trial so the advertised origin names where the - * trial is actually served. + * BASE URL for the advertised `resource.url`, derived from the committed spec — the same + * pattern as the llm twin, and the published terms themselves. There is deliberately no env + * override anymore: `SEARCH_SERVICE_URL` was a trial-period mechanism (advertise where the + * trial was actually served) and its leftover value kept advertising the `*.bunny.run` origin + * in production, which indexers then could not attribute to web-agent.fretchen.eu. The + * resource url is the payment scope, not the serving origin — it keeps naming the production + * identity wherever the script is served from. */ -const SERVICE_URL = process.env.SEARCH_SERVICE_URL ?? "https://web-agent.fretchen.eu"; +const SERVICE_URL = openapiSpec.servers[0].url; /** The favicon as raw bytes — web Response bodies are bytes, not Scaleway base64 strings. */ const FAVICON_BYTES = Uint8Array.from(atob(faviconBase64), (c) => c.charCodeAt(0)); diff --git a/bunny_js/test/routes.test.ts b/bunny_js/test/routes.test.ts index 160d186ed..cbe43b1b4 100644 --- a/bunny_js/test/routes.test.ts +++ b/bunny_js/test/routes.test.ts @@ -588,7 +588,9 @@ describe("the 402 challenge", () => { const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); const { resource } = JSON.parse(res.body) as { resource: { url: string } }; - expect(resource.url).toMatch(/\/search$/); + // The full production origin from the committed spec — never a serving origin (a leftover + // trial env once advertised the *.bunny.run URL here, which indexers could not attribute). + expect(resource.url).toBe("https://web-agent.fretchen.eu/search"); }); /** One bazaar block per route (docs.x402.org/extensions/bazaar): the block names the method From 65c385d75c7dd3e62a89d6782e760fbbd7eae6b6 Mon Sep 17 00:00:00 2001 From: fretchen Date: Wed, 11 Nov 2026 08:28:37 +0100 Subject: [PATCH 2/8] agents: agent-facing root pages + capability-first names Each origin now serves a generated agent README at GET /, unconditionally (agents send Accept: */*, so the old text/html gate hid it): spec title and description, the well-known contracts (openapi.json, agent-registration, oasf.json, MCP), prices from the spec's x-payment-info / the live price ceiling, the x402 payment flow, a no-wallet curl that returns the honest 402, and the error semantics. The x402scan parses stays the first head element; nothing on the page is hand-restated. - discovery.ts (bunny + scw twins): FAVICON_DISCOVERY_HTML + wantsHtml replaced by buildRootPage(RootPageInput); no callers of wantsHtml remain. - searchapi_routes: page generated from the committed spec (prices per route from x-payment-info); serviceName -> "Fretchen Web Retrieval"; spec title -> "Fretchen Web Retrieval - pay-per-call web access for agents". - llm_routes: page with the live MAX_PRICE_ATOMIC ceiling, the testnet mock note, and the MCP link; serviceName -> "Fretchen LLM Inference"; spec title -> "Fretchen LLM Inference - pay-per-call chat completions for agents". - mcp_routes: hand-composed gateway page (the origin relays, holds no spec of its own): links to both seller origins, its registration document, free initialize/tools/list curl, and the isError/structuredContent 402 shape of an unpaid tools/call. - genimg (scw): same scheme, generated from openapi.genimg.json (title and description unchanged - already capability-first, decided). - tests: root serving is now unconditional and asserted with favicon-first, href, price and curl checks; the llm "non-POST" rejection moved from GET (now the root page) to PUT; pinned serviceNames and titles updated. - READMEs: one entry each describing the root page. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- bunny_js/README.md | 7 +++ bunny_js/src/discovery.ts | 73 +++++++++++++++++-------- bunny_js/src/llm_routes.ts | 48 +++++++++++++++-- bunny_js/src/mcp_routes.ts | 48 +++++++++++++++-- bunny_js/src/openapi.llm.json | 4 +- bunny_js/src/openapi.search.json | 2 +- bunny_js/src/searchapi_routes.ts | 54 +++++++++++++++++-- bunny_js/test/llm.test.ts | 60 +++++++++++---------- bunny_js/test/mcp.test.ts | 34 ++++++++---- bunny_js/test/routes.test.ts | 40 +++++++++----- scw_js/README.md | 1 + scw_js/discovery.ts | 76 +++++++++++++++++++-------- scw_js/genimg_x402_token.ts | 53 ++++++++++++++++--- scw_js/test/genimg_x402_token.test.ts | 63 +++++++++++----------- 14 files changed, 416 insertions(+), 147 deletions(-) diff --git a/bunny_js/README.md b/bunny_js/README.md index 4037e19ca..52c039f89 100644 --- a/bunny_js/README.md +++ b/bunny_js/README.md @@ -75,6 +75,13 @@ dropped transaction the nonce manager has already counted: a restart of the isol envelope helpers are not ported — there is no Scaleway envelope here. - `src/runtime-shims.ts` — pins the Node `Buffer`/`process` globals the ported code assumes, imported FIRST by the entry so they exist before any ported module's top-level code runs. +- **Agent root page** — every origin serves a root page at `GET /`, unconditionally (agents + send `Accept: */*`, so a text/html gate hid the page from its reader). Built by + [`src/discovery.ts`](./src/discovery.ts)'s `buildRootPage` from what the seller already + serves — spec title/description, `x-payment-info` prices, the well-known contracts, the + no-wallet curl that returns the honest 402 — with the `` x402scan parses + kept as the first head element. Nothing on the page is hand-restated. The genimg origin + serves the same page scheme from `scw_js` (its `discovery.ts` twin). - `src/facilitator/` — the facilitator's own corner of the tree (`src/` is otherwise the twins'): the payment core ported **verbatim** from `x402_facilitator/` (that package keeps its own copy alive for its Scaleway function until the teardown PR; only `walletreportcron` diff --git a/bunny_js/src/discovery.ts b/bunny_js/src/discovery.ts index 2e5653e14..4b488eb95 100644 --- a/bunny_js/src/discovery.ts +++ b/bunny_js/src/discovery.ts @@ -7,33 +7,64 @@ * from the HTML, and only then probing COMMON_FAVICON_PATHS * (/favicon.ico, /favicon.png, /favicon.svg). We satisfy the root-HTML path here and * point it at /favicon.png (which, unlike /favicon.ico, does reach the function). + * + * The root page doubles as the agent-facing "README": an agent that lands on the origin + * (curl, MCP client, ERC-8004 shepherd) should learn what the service is, where the + * machine-readable contracts live, what a call costs, how to pay, and what a 402 means — + * in one fetch. Every fact on the page is GENERATED by the caller from the same committed + * constants/files that serve it (spec title/description, `x-payment-info`, endpoint + * constants), never hand-restated: a restated fact is the next silent drift bug. Surface + * area is deliberately minimal — human-oriented prose lives on the website, this page is + * the last mile from discovery to a first (unpaid) honest 402. */ -/** Minimal HTML served at the origin root so scrapers can discover the favicon. */ -export const FAVICON_DISCOVERY_HTML = ` +/** The fields `buildRootPage` renders; `endpoints` carries absolute hrefs. */ +export interface RootPageInput { + title: string; + summary: string; + endpoints: { href: string; label: string }[]; + /** One line per route/price pair, generated from the spec's `x-payment-info`. */ + price: string[]; + payment: string; + /** The no-wallet first look — usually the curl that returns the honest 402. */ + tryIt: string; + errors: string; +} + +const escapeHtml = (text: string): string => + text.replace(/&/g, "&").replace(//g, ">"); + +/** The agent-facing root page. The favicon link stays first — x402scan parses it. */ +export function buildRootPage(input: RootPageInput): string { + return ` -Fretchen x402 service +${escapeHtml(input.title)} - + +

${escapeHtml(input.title)}

+

${escapeHtml(input.summary)}

+

Endpoints

+
    +${input.endpoints + .map( + (e) => + `
  • ${escapeHtml(e.href)} — ${escapeHtml(e.label)}
  • `, + ) + .join("\n")} +
+

Price

+
    +${input.price.map((p) => `
  • ${escapeHtml(p)}
  • `).join("\n")} +
+

Payment

+

${escapeHtml(input.payment)}

+

Try it

+
${escapeHtml(input.tryIt)}
+

Errors

+

${escapeHtml(input.errors)}

+ `; - -/** - * True when the client is asking for HTML (a browser or discovery scraper), rather than - * an x402 payment client — those POST JSON and never send `Accept: text/html`. Used to - * decide whether a GET on the origin root should return the discovery HTML instead of the - * normal "only POST" rejection. Header lookup is case-insensitive. - */ -export function wantsHtml(headers: Record | undefined): boolean { - if (!headers) { - return false; - } - for (const [key, value] of Object.entries(headers)) { - if (key.toLowerCase() === "accept") { - return typeof value === "string" && value.toLowerCase().includes("text/html"); - } - } - return false; } diff --git a/bunny_js/src/llm_routes.ts b/bunny_js/src/llm_routes.ts index c48cac93d..cc56c0624 100644 --- a/bunny_js/src/llm_routes.ts +++ b/bunny_js/src/llm_routes.ts @@ -53,9 +53,10 @@ import { type SdkPaymentPayload, type SdkPaymentRequirements, } from "./x402_server.js"; -import { FAVICON_DISCOVERY_HTML, wantsHtml } from "./discovery.js"; +import { buildRootPage } from "./discovery.js"; import { AGENT_REGISTRATION_PATH, + MCP_SERVICE, OASF_MANIFEST_PATH, buildAgentRegistration, buildOasfRecord, @@ -73,6 +74,44 @@ export const LLM_COMPLETION_BOUND_SECONDS = 40; /** The favicon as raw bytes — web Response bodies are bytes, not Scaleway base64 strings. */ const FAVICON_BYTES = Uint8Array.from(atob(faviconBase64), (c) => c.charCodeAt(0)); +/** + * The agent-facing root page, generated from the same constants/files that serve the twin: + * title/summary from the committed spec, the price ceiling from the live MAX_PRICE_ATOMIC + * constant (the static spec's x-payment-info max is a documentation baseline that is + * overridden at serve time — the page must show the real one), links to the well-known + * contracts. Nothing is hand-restated — see discovery.ts. + */ +const ROOT_PAGE = buildRootPage({ + title: openapiSpec.info.title, + summary: openapiSpec.info.description, + endpoints: [ + { + href: `${openapiSpec.servers[0].url}/openapi.json`, + label: "OpenAPI 3.1 spec — request/response schemas and payment terms", + }, + { + href: `${openapiSpec.servers[0].url}/${AGENT_REGISTRATION_PATH}`, + label: "ERC-8004 registration (identity, services, wallet)", + }, + { + href: `${openapiSpec.servers[0].url}/${OASF_MANIFEST_PATH}`, + label: "OASF capability record (skills, domains)", + }, + { + href: MCP_SERVICE.endpoint, + label: "stateless MCP endpoint — same tools as JSON-RPC 2.0 over POST (chat_completion)", + }, + ], + price: [ + `POST / — metered by real token usage, at most $${formatUsdcAtomicAsDecimalUsd(MAX_PRICE_ATOMIC.USDC)} per request`, + ], + payment: + "x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — exact terms (scheme batch-settlement, network, token, ceiling) are in the challenge, then retry the same request with the payment attached.", + tryIt: `curl -X POST '${openapiSpec.servers[0].url}/' -H 'Content-Type: application/json' -d '{"model":"${advertisedModelIds()[0]}","messages":[{"role":"user","content":"ping"}]}'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.\n# Testnet note: on Base Sepolia the payment settles and the completion is a mocked answer — a free end-to-end test.`, + errors: + "402 = unpaid or invalid payment (challenge in the Payment-Required header); 400 = invalid request (OpenAI-shaped error body); 404 = unknown model. Response schemas: openapi.json.", +}); + function jsonError(statusCode: number, error: string): Response { return new Response(JSON.stringify({ error }), { status: statusCode, headers: CORS_HEADERS }); } @@ -158,8 +197,9 @@ export async function handleLLMRequest(request: Request): Promise { }); } - if (isGetOrHead && path === "" && wantsHtml(headers)) { - return new Response(isHead ? null : FAVICON_DISCOVERY_HTML, { + if (isGetOrHead && path === "") { + // Unconditional on purpose: the reader is an agent, and agents send Accept: */* too. + return new Response(isHead ? null : ROOT_PAGE, { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "text/html; charset=utf-8" }, }); @@ -205,7 +245,7 @@ export async function handleLLMRequest(request: Request): Promise { networks: getBatchSettlementNetworks(), maxTimeoutSeconds: LLM_MAX_TIMEOUT_SECONDS, extensions: buildLlmBazaarExtension(), - serviceName: "Fretchen AI Assistant", + serviceName: "Fretchen LLM Inference", tags: ["llm", "chat", "x402"], iconUrl: `${openapiSpec.servers[0].url}/favicon.png`, }); diff --git a/bunny_js/src/mcp_routes.ts b/bunny_js/src/mcp_routes.ts index 1a9dab915..29d187938 100644 --- a/bunny_js/src/mcp_routes.ts +++ b/bunny_js/src/mcp_routes.ts @@ -28,10 +28,49 @@ * does not do, per the MCP spec's session-less allowance. */ import { CORS_HEADERS } from "./utils.js"; -import { FAVICON_DISCOVERY_HTML, wantsHtml } from "./discovery.js"; +import { buildRootPage } from "./discovery.js"; import { faviconBase64, faviconContentType } from "./favicon.js"; import { MCP_TOOLS } from "./mcp_tools.js"; -import { AGENT_REGISTRATION_PATH, buildMcpOriginRegistration } from "./agent_registration.js"; +import { + AGENT_REGISTRATION_PATH, + MCP_SERVICE, + buildMcpOriginRegistration, +} from "./agent_registration.js"; + +/** + * The agent-facing gateway page. This origin relays to the twin sellers and holds no spec + * of its own, so the page is hand-composed (no spec to generate from): it points at the + * two service origins — the single sources of truth for their schemas, prices and 402 + * terms — plus this origin's own registration document and the MCP protocol curls. See + * discovery.ts for the generated-not-restated rule. + */ +const ROOT_PAGE = buildRootPage({ + title: "Fretchen MCP Gateway — pay-per-call tools for agents", + summary: `A stateless MCP endpoint (Streamable HTTP, JSON-RPC 2.0 over POST) relaying ${MCP_TOOLS.length} paid tools. No sessions, no server-initiated streams: initialize and tools/list are free; tools/call relays to the seller, whose x402 terms apply — an unpaid call comes back as a tool result with isError, the payment demands in structuredContent.`, + endpoints: [ + { + href: "https://llm-agent.fretchen.eu/", + label: "chat_completion — the LLM seller's page, spec and terms", + }, + { + href: "https://web-agent.fretchen.eu/", + label: "web_search — the web seller's page, spec and terms", + }, + { + href: `https://mcp-agent.fretchen.eu/${AGENT_REGISTRATION_PATH}`, + label: "this origin's registration document (domain control proof for both agents)", + }, + ], + price: MCP_TOOLS.map( + (tool) => + `${tool.name} — priced at the seller it relays to; the unpaid call's tool result carries the exact 402 terms`, + ), + payment: + "per tool, via x402 at the relaying seller: no account, no API key. initialize and tools/list are free; an unpaid tools/call returns the 402 demands as structuredContent in an isError tool result — retry with the payment attached.", + tryIt: `curl -X POST '${MCP_SERVICE.endpoint}' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'\n# → the tools and their input schemas, free (as is initialize). tools/call carries the x402 payment.`, + errors: + "JSON-RPC errors: -32601 unknown method (a stateless server: initialize, tools/list, tools/call only), -32602 unknown tool. GET /mcp is 405 Allow: POST (the optional GET stream is not offered). Unpaid tools/call is NOT an error — it is a tools result with isError and the 402 demands in structuredContent.", +}); /** The latest protocol revision this server implements. */ const DEFAULT_PROTOCOL_VERSION = "2025-06-18"; @@ -201,8 +240,9 @@ export async function handleMCPRequest(request: Request): Promise { // of the scw original's blanket 405. if (method === "GET" || method === "HEAD") { const isHead = method === "HEAD"; - if (path === "" && wantsHtml(Object.fromEntries(request.headers))) { - return new Response(isHead ? null : FAVICON_DISCOVERY_HTML, { + if (path === "") { + // Unconditional on purpose: the reader is an agent, and agents send Accept: */* too. + return new Response(isHead ? null : ROOT_PAGE, { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "text/html; charset=utf-8" }, }); diff --git a/bunny_js/src/openapi.llm.json b/bunny_js/src/openapi.llm.json index 0ef4f1480..6dc1b498b 100644 --- a/bunny_js/src/openapi.llm.json +++ b/bunny_js/src/openapi.llm.json @@ -1,8 +1,8 @@ { "openapi": "3.1.0", "info": { - "title": "Fretchen AI Assistant (LLM) Service", - "description": "AI chat assistant, paid via x402 batch-settlement payment channels in USDC, EURC or EURe on Base (USDC everywhere; the euro tokens on Base).", + "title": "Fretchen LLM Inference — pay-per-call chat completions for agents", + "description": "OpenAI-shaped chat completions from Mistral models, sold per request to autonomous agents via x402 batch-settlement payment channels — no account, no API key. Metered by real token usage under a fixed per-request ceiling; settled in USDC (Base or Optimism), EURC or EURe (Base), while Base Sepolia settles for free and answers with a mocked completion.", "version": "1.0.0", "x-guidance": "OpenAI chat-completions body. POST / with { model, messages: [{ role, content }, ...] } and no payment header to receive a 402 with x402 batch-settlement payment requirements (accepts[]). Open/top up a payment channel per the requirements, retry with the payment header, and the service returns a standard OpenAI chat.completion object. Streaming (stream: true) is not supported. Each message is metered and settled up to a per-message price ceiling; the real cost is usage-derived and typically lower. Other standard OpenAI chat params (temperature, top_p, stop, seed, the penalties, response_format) are forwarded to the upstream model and work as normal; they are not enumerated here because the upstream owns that contract. The exceptions are stream, n and max_tokens, which are rejected with a 400 rather than ignored, because each would move cost past the fixed per-message ceiling this endpoint meters against. Tool calling is supported (see x-capabilities): send OpenAI-shaped tools, and a tool call comes back as choices[].message.tool_calls with finish_reason: \"tool_calls\" and content: null. Execute it yourself and send the result back as a role:\"tool\" message with the matching tool_call_id — this endpoint never calls a tool on your behalf, and accepts no callback or MCP server URL. Note: payment uses x402 batch-settlement (a stateful channel), so a stock OpenAI SDK cannot pay this endpoint without batch-settlement client wiring — the OpenAI shape is for body legibility, not drop-in SDK use.", "contact": { diff --git a/bunny_js/src/openapi.search.json b/bunny_js/src/openapi.search.json index 5c32745ad..fe2f87558 100644 --- a/bunny_js/src/openapi.search.json +++ b/bunny_js/src/openapi.search.json @@ -1,7 +1,7 @@ { "openapi": "3.1.0", "info": { - "title": "Fretchen Web Access Service", + "title": "Fretchen Web Retrieval — pay-per-call web access for agents", "description": "Web search and single-page fetch for agents, paid per call via x402 batch-settlement payment channels in USDC (or EURC on Base).", "version": "1.0.0", "x-guidance": "GET /search?q=... or GET /fetch?url=... with no payment header to receive a 402 carrying x402 batch-settlement payment requirements (accepts[]). Open or top up a payment channel per those requirements, retry with the payment header, and the service returns JSON. Payment travels in the header only — these are GET routes, so there is no body fallback. Both routes bill onto the SAME channel as llm-agent.fretchen.eu when the payer, voucher signer and receiver match, so an agent already chatting there pays for web access without a second deposit. Prices are fixed per route and settled only when the call succeeds: an upstream failure returns 500 and takes no money. Mainnet only — testnet USDC is free and these routes spend real money on the caller's behalf.", diff --git a/bunny_js/src/searchapi_routes.ts b/bunny_js/src/searchapi_routes.ts index 4239910a6..d311f1827 100644 --- a/bunny_js/src/searchapi_routes.ts +++ b/bunny_js/src/searchapi_routes.ts @@ -24,9 +24,10 @@ import { type Route, } from "./search_schemas.js"; import { CORS_HEADERS } from "./utils.js"; -import { FAVICON_DISCOVERY_HTML, wantsHtml } from "./discovery.js"; +import { buildRootPage } from "./discovery.js"; import { AGENT_REGISTRATION_PATH, + MCP_SERVICE, OASF_MANIFEST_PATH, buildAgentRegistration, buildOasfRecord, @@ -75,6 +76,48 @@ const MAX_TIMEOUT_SECONDS = 30; */ const SERVICE_URL = openapiSpec.servers[0].url; +/** + * The agent-facing root page, generated from the same constants/files that serve the twin: + * title/summary from the committed spec, prices from its `x-payment-info` per route, links to + * the well-known contracts. Nothing is hand-restated — see discovery.ts. + */ +const ROOT_PAGE = buildRootPage({ + title: openapiSpec.info.title, + summary: openapiSpec.info.description, + endpoints: [ + { + href: `${SERVICE_URL}/openapi.json`, + label: "OpenAPI 3.1 spec — request/response schemas and payment terms", + }, + { + href: `${SERVICE_URL}/${AGENT_REGISTRATION_PATH}`, + label: "ERC-8004 registration (identity, services, wallet)", + }, + { + href: `${SERVICE_URL}/${OASF_MANIFEST_PATH}`, + label: "OASF capability record (skills, domains)", + }, + { + href: MCP_SERVICE.endpoint, + label: "stateless MCP endpoint — same tools as JSON-RPC 2.0 over POST (web_search)", + }, + ], + price: ROUTES.map((route) => { + const info = ( + openapiSpec.paths as Record< + string, + Record + > + )[`/${route}`].get["x-payment-info"]; + return `GET /${route} — $${info?.price.amount ?? "?"} per call`; + }), + payment: + "x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — exact terms (scheme batch-settlement, network, token, amount) are in the challenge, then retry the same request with the payment attached.", + tryIt: `curl '${SERVICE_URL}/search?q=erc-8004'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.`, + errors: + "402 = unpaid (challenge in the Payment-Required header); 404 = no such route; everything else is standard HTTP. Response schemas: openapi.json.", +}); + /** The favicon as raw bytes — web Response bodies are bytes, not Scaleway base64 strings. */ const FAVICON_BYTES = Uint8Array.from(atob(faviconBase64), (c) => c.charCodeAt(0)); @@ -315,7 +358,7 @@ async function challenge(route: Route, receiverAddress: `0x${string}`): Promise< // One bazaar block per route: the block names the method and the query parameters, and the // two routes sell different things at different prices. (Copied.) extensions: buildSearchBazaarExtension(route), - serviceName: "Fretchen Web Access", + serviceName: "Fretchen Web Retrieval", tags: ["search", "web", "x402"], iconUrl: `${openapiSpec.servers[0].url}/favicon.png`, }); @@ -334,7 +377,7 @@ export async function handleRequest(request: Request): Promise { } // Web Request headers are lowercase; every consumer in the ported stack - // (extractPaymentPayload, wantsHtml) matches case-insensitively. + // (extractPaymentPayload) matches case-insensitively. const headers = Object.fromEntries(request.headers) as Record; const method = request.method; const url = new URL(request.url); @@ -385,8 +428,9 @@ export async function handleRequest(request: Request): Promise { }); } - if (isGetOrHead && path === "" && wantsHtml(headers)) { - return new Response(isHead ? null : FAVICON_DISCOVERY_HTML, { + if (isGetOrHead && path === "") { + // Unconditional on purpose: the reader is an agent, and agents send Accept: */* too. + return new Response(isHead ? null : ROOT_PAGE, { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "text/html; charset=utf-8" }, }); diff --git a/bunny_js/test/llm.test.ts b/bunny_js/test/llm.test.ts index afdabe6c6..046f63051 100644 --- a/bunny_js/test/llm.test.ts +++ b/bunny_js/test/llm.test.ts @@ -262,8 +262,8 @@ describe("sc_llm_x402", () => { expect(res.headers["access-control-expose-headers"]).toContain("Payment-Response"); }); - it("returns 400 for a non-POST method", async () => { - const res = await handle(makeEvent({ httpMethod: "GET" }) as never, {}); + it("returns 400 for a non-POST, non-GET method", async () => { + const res = await handle(makeEvent({ httpMethod: "PUT" }) as never, {}); expect(res.statusCode).toBe(400); }); @@ -392,31 +392,35 @@ describe("sc_llm_x402", () => { expect(res.body).toBe(""); }); - // x402scan fetches the origin root and parses before probing any - // favicon path. An HTML-accepting GET on "/" must return that discovery HTML. - it("serves favicon-discovery HTML on GET / when the client accepts HTML", async () => { - const res = await handle( - makeEvent({ - httpMethod: "GET", - path: "/", - headers: { accept: "text/html,application/xhtml+xml" }, - }) as never, - {}, - ); - expect(res.statusCode).toBe(200); - expect(res.headers["content-type"]).toContain("text/html"); - expect(res.body).toContain('rel="icon"'); - expect(res.body).toContain("/favicon.png"); - }); - - // An x402 payment client GETs / without an HTML Accept header — it must fall through - // to the normal "only POST" rejection, not receive HTML. - it("does NOT serve HTML on GET / without an HTML Accept header", async () => { - const res = await handle( - makeEvent({ httpMethod: "GET", path: "/", headers: {} }) as never, - {}, - ); - expect(res.statusCode).toBe(400); + // The root page is the agent-facing README, served unconditionally — agents send + // Accept: */* too, so gating on text/html hid the whole page from the reader it is + // for. x402scan still gets its first; the page asserts favicon + // link, contracts, price, curl and errors below. + it("serves the agent-facing root page on GET / regardless of the Accept header", async () => { + for (const accept of [undefined, "*/*", "text/html"]) { + const res = await handle( + makeEvent({ + httpMethod: "GET", + path: "/", + ...(accept ? { headers: { accept } } : {}), + }) as never, + {}, + ); + expect(res.statusCode).toBe(200); + expect(res.headers["content-type"]).toContain("text/html"); + // x402scan parses from this document — it must stay first. + expect(res.body.indexOf('rel="icon"')).toBeLessThan(res.body.indexOf("")); + expect(res.body).toContain("/favicon.png"); + // The page title is the spec's — pinned here so a title change updates the test. + expect(res.body).toContain("Fretchen LLM Inference"); + expect(res.body).toContain("/openapi.json"); + expect(res.body).toContain("agent-registration.json"); + expect(res.body).toContain("/.well-known/oasf.json"); + expect(res.body).toContain("https://mcp-agent.fretchen.eu/mcp"); + expect(res.body).toContain("POST /"); + expect(res.body).toContain("curl -X POST"); + expect(res.body).toContain("402"); + } }); // Regression guard: x-payment-info.price.max must track the real, live price @@ -581,7 +585,7 @@ describe("sc_llm_x402", () => { }), }), }), - serviceName: "Fretchen AI Assistant", + serviceName: "Fretchen LLM Inference", tags: ["llm", "chat", "x402"], iconUrl: "https://llm-agent.fretchen.eu/favicon.png", }), diff --git a/bunny_js/test/mcp.test.ts b/bunny_js/test/mcp.test.ts index 91fdce288..2fc276c9d 100644 --- a/bunny_js/test/mcp.test.ts +++ b/bunny_js/test/mcp.test.ts @@ -148,7 +148,7 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { /** * The DECIDED deviation from the scw original: GET answers like a normal origin * instead of a blanket 405 — 404 with the honest stateless notice for paths with no - * content, 200 discovery HTML at the root when the caller accepts HTML, and DELETE + * content, 200 agent-facing root page at the root (unconditional), and DELETE * keeps 405 (session termination is genuinely not a thing here, per the MCP spec). */ /** @@ -214,16 +214,28 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { expect(await head.text()).toBe(""); }); - it("serves discovery HTML at the origin root when the caller accepts HTML", async () => { - const res = await handle( - new Request("https://mcp-agent.fretchen.eu/", { - method: "GET", - headers: { Accept: "text/html" }, - }), - ); - expect(res.status).toBe(200); - expect(res.headers.get("content-type")).toContain("text/html"); - expect(await res.text()).toContain('rel="icon"'); + /** The root page is the agent-facing gateway README, served unconditionally — agents + * send "Accept: star-slash" too. x402scan still gets its <link rel="icon"> first. */ + it("serves the agent-facing gateway page at the origin root regardless of the Accept header", async () => { + for (const headers of [undefined, { Accept: "*/*" }, { Accept: "text/html" }]) { + const res = await handle( + new Request("https://mcp-agent.fretchen.eu/", { method: "GET", headers }), + ); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/html"); + const body = await res.text(); + // x402scan parses <link rel="icon"> from this document — it must stay first. + expect(body.indexOf('rel="icon"')).toBeLessThan(body.indexOf("<title>")); + expect(body).toContain("Fretchen MCP Gateway"); + // The gateway points at the two sellers, its domain-proof registration doc, and + // the free first look: initialize and tools/list respond unpaid. + expect(body).toContain("https://llm-agent.fretchen.eu/"); + expect(body).toContain("https://web-agent.fretchen.eu/"); + expect(body).toContain(".well-known/agent-registration.json"); + expect(body).toContain("https://mcp-agent.fretchen.eu/mcp"); + expect(body).toContain("tools/list"); + expect(body).toContain("402"); + } }); it("answers DELETE with 405 Allow: POST — no sessions to terminate", async () => { diff --git a/bunny_js/test/routes.test.ts b/bunny_js/test/routes.test.ts index cbe43b1b4..bb8972b9c 100644 --- a/bunny_js/test/routes.test.ts +++ b/bunny_js/test/routes.test.ts @@ -522,20 +522,32 @@ describe("discovery", () => { expect(await res.text()).toBe(""); }); - test("serves the discovery HTML at the root when the caller accepts HTML", async () => { - const res = await handle(discoveryRequest("GET", "", { Accept: "text/html" })); + /** The root page is the agent-facing README, served unconditionally — agents send + * "Accept: star-slash" too, so gating on text/html hid the whole page from the reader + * it is for. x402scan still gets its <link rel="icon"> first. */ + test("serves the agent-facing root page at the origin root, regardless of the Accept header", async () => { + const headerCases: Record<string, string>[] = [{}, { Accept: "*/*" }, { Accept: "text/html" }]; + for (const headers of headerCases) { + const res = await handle(discoveryRequest("GET", "", headers)); - expect(res.statusCode).toBe(200); - expect(res.headers["content-type"]).toContain("text/html"); - expect(res.body).toContain('rel="icon"'); - }); - - /** Only when HTML is asked for. A bare root request is not a browser or a crawler looking for an - * icon, and answering HTML to it would hide the fact that the root serves no API. */ - test("does not serve HTML at the root without an HTML Accept header", async () => { - const res = await handle(discoveryRequest("GET", "")); - - expect(res.statusCode).toBe(404); + expect(res.statusCode).toBe(200); + expect(res.headers["content-type"]).toContain("text/html"); + // x402scan parses <link rel="icon"> from this document — it must stay first. + expect(res.body.indexOf('rel="icon"')).toBeLessThan(res.body.indexOf("<title>")); + expect(res.body).toContain("/favicon.png"); + // The page title is the spec's — pinned so a title change updates this test. + expect(res.body).toContain("Fretchen Web Retrieval"); + expect(res.body).toContain("/openapi.json"); + expect(res.body).toContain(".well-known/agent-registration.json"); + expect(res.body).toContain(".well-known/oasf.json"); + expect(res.body).toContain("https://mcp-agent.fretchen.eu/mcp"); + // Prices restated on the page must be the spec's x-payment-info values. + expect(res.body).toContain("GET /search — $0.01 per call"); + expect(res.body).toContain("GET /fetch — $0.001 per call"); + expect(res.body).toContain("curl 'https://web-agent.fretchen.eu/search?q=erc-8004'"); + expect(res.body).toContain("402"); + expect(mockCreateBatchSettlementPaymentRequirements).not.toHaveBeenCalled(); + } }); /** Discovery must not open a hole in the route check: an unknown path is still 404, and still @@ -617,7 +629,7 @@ describe("the 402 challenge", () => { expect(fetchOpts.extensions.bazaar.info.input.method).toBe("GET"); expect(Object.keys(fetchOpts.extensions.bazaar.info.input.queryParams)).toEqual(["url"]); for (const opts of [searchOpts, fetchOpts]) { - expect(opts.serviceName).toBe("Fretchen Web Access"); + expect(opts.serviceName).toBe("Fretchen Web Retrieval"); expect(opts.tags).toEqual(["search", "web", "x402"]); expect(opts.iconUrl).toBe("https://web-agent.fretchen.eu/favicon.png"); } diff --git a/scw_js/README.md b/scw_js/README.md index 35c8a7784..bd9d3dc56 100644 --- a/scw_js/README.md +++ b/scw_js/README.md @@ -11,6 +11,7 @@ the **channel cron**, and the **Growth API**. - **OpenAPI Spec**: [`openapi.genimg.json`](./openapi.genimg.json) - the x402 discovery contract, served live at `GET /openapi.json` on the origin - **EIP-8004 Registration**: built per service by [`agent_registration.ts`](./agent_registration.ts), served live at `GET /.well-known/agent-registration.json` on each origin (see [ERC-8004 agent registration](#erc-8004-agent-registration)) +- **Agent root page**: served unconditionally at `GET /` on each origin — generated from the committed spec by [`discovery.ts`](./discovery.ts)'s `buildRootPage` (title, prices from `x-payment-info`, contracts, the no-wallet curl returning the honest 402; the `<link rel="icon">` x402scan parses stays first). Nothing on the page is hand-restated. ### Quick Links diff --git a/scw_js/discovery.ts b/scw_js/discovery.ts index 2e5653e14..af0e5fc48 100644 --- a/scw_js/discovery.ts +++ b/scw_js/discovery.ts @@ -7,33 +7,67 @@ * <link rel="icon"> from the HTML, and only then probing COMMON_FAVICON_PATHS * (/favicon.ico, /favicon.png, /favicon.svg). We satisfy the root-HTML path here and * point it at /favicon.png (which, unlike /favicon.ico, does reach the function). + * + * The root page doubles as the agent-facing "README": an agent that lands on the origin + * (curl, MCP client, ERC-8004 shepherd) should learn what the service is, where the + * machine-readable contracts live, what a call costs, how to pay, and what a 402 means — + * in one fetch. Every fact on the page is GENERATED by the caller from the same committed + * constants/files that serve it (spec title/description, `x-payment-info`, endpoint + * constants), never hand-restated: a restated fact is the next silent drift bug. Surface + * area is deliberately minimal — human-oriented prose lives on the website, this page is + * the last mile from discovery to a first (unpaid) honest 402. + * + * The bunny twin of this file is `bunny_js/src/discovery.ts` (kept in sync; that one is + * restricted to APIs the bunny Deno 1.x isolate reliably exposes). */ -/** Minimal HTML served at the origin root so scrapers can discover the favicon. */ -export const FAVICON_DISCOVERY_HTML = `<!doctype html> +/** The fields `buildRootPage` renders; `endpoints` carries absolute hrefs. */ +export interface RootPageInput { + title: string; + summary: string; + endpoints: { href: string; label: string }[]; + /** One line per route/price pair, generated from the spec's `x-payment-info`. */ + price: string[]; + payment: string; + /** The no-wallet first look — usually the curl that returns the honest 402. */ + tryIt: string; + errors: string; +} + +const escapeHtml = (text: string): string => + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + +/** The agent-facing root page. The favicon link stays first — x402scan parses it. */ +export function buildRootPage(input: RootPageInput): string { + return `<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <link rel="icon" href="/favicon.png" type="image/jpeg"> -<title>Fretchen x402 service +${escapeHtml(input.title)} - + +

${escapeHtml(input.title)}

+

${escapeHtml(input.summary)}

+

Endpoints

+
    +${input.endpoints + .map( + (e) => + `
  • ${escapeHtml(e.href)} — ${escapeHtml(e.label)}
  • `, + ) + .join("\n")} +
+

Price

+
    +${input.price.map((p) => `
  • ${escapeHtml(p)}
  • `).join("\n")} +
+

Payment

+

${escapeHtml(input.payment)}

+

Try it

+
${escapeHtml(input.tryIt)}
+

Errors

+

${escapeHtml(input.errors)}

+ `; - -/** - * True when the client is asking for HTML (a browser or discovery scraper), rather than - * an x402 payment client — those POST JSON and never send `Accept: text/html`. Used to - * decide whether a GET on the origin root should return the discovery HTML instead of the - * normal "only POST" rejection. Header lookup is case-insensitive. - */ -export function wantsHtml(headers: Record | undefined): boolean { - if (!headers) { - return false; - } - for (const [key, value] of Object.entries(headers)) { - if (key.toLowerCase() === "accept") { - return typeof value === "string" && value.toLowerCase().includes("text/html"); - } - } - return false; } diff --git a/scw_js/genimg_x402_token.ts b/scw_js/genimg_x402_token.ts index 557d48ca4..fcdca287f 100644 --- a/scw_js/genimg_x402_token.ts +++ b/scw_js/genimg_x402_token.ts @@ -42,7 +42,7 @@ import { validatePaymentNetwork, getExpectedNetworks } from "./getChain.js"; import type { ScwEvent } from "./types.js"; import openapiSpec from "./openapi.genimg.json" with { type: "json" }; import { faviconBase64, faviconContentType } from "./favicon.js"; -import { FAVICON_DISCOVERY_HTML, wantsHtml } from "./discovery.js"; +import { buildRootPage } from "./discovery.js"; import { buildGenimgBazaarExtension } from "./bazaar_discovery.js"; import { AGENT_REGISTRATION_PATH, @@ -65,6 +65,47 @@ const PRICE_ATOMIC: PriceList = { }; const GAS_BUFFER = parseEther("0.00001"); +/** + * The agent-facing root page, generated from the same constants/files that serve the + * handler: title/summary from the committed spec, the price from its `x-payment-info`, + * links to the well-known contracts. Nothing is hand-restated — see discovery.ts. + */ +const ROOT_PAGE = buildRootPage({ + title: openapiSpec.info.title, + summary: openapiSpec.info.description, + endpoints: [ + { + href: `${openapiSpec.servers[0].url}/openapi.json`, + label: "OpenAPI 3.1 spec — request/response schemas and payment terms", + }, + { + href: `${openapiSpec.servers[0].url}/${AGENT_REGISTRATION_PATH}`, + label: "ERC-8004 registration (identity, services, wallet)", + }, + { + href: `${openapiSpec.servers[0].url}/${OASF_MANIFEST_PATH}`, + label: "OASF capability record (skills, domains)", + }, + ], + price: [ + ((): string => { + // Generated from the committed spec, not restated — the page and the 402 share it. + const info = ( + openapiSpec.paths as Record< + string, + Record + > + )["/"].post["x-payment-info"]; + return `POST /imagegen — $${info?.price.amount ?? "?"} per image (USDC, EURC or EURe; each token's price is set in its own units)`; + })(), + ], + payment: + "x402 (HTTP 402), exact scheme — one signature moves the money per call: no account, no API key, no approve() needed. An unpaid request answers 402 with the machine-readable Payment-Required challenge — exact terms (network, token, amount) are in the challenge, then retry the same request with the payment attached.", + tryIt: `curl -X POST '${openapiSpec.servers[0].url}/imagegen' -H 'Content-Type: application/json' -d '{"prompt":"a lighthouse at dusk"}'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.`, + errors: + "402 = unpaid (challenge in the Payment-Required header); 400 = invalid request (OpenAI-shaped error body); 500 = server configuration error. Response schemas: openapi.json.", +}); + // keccak256("Transfer(address,address,uint256)") — used to extract tokenId from mint tx logs const TRANSFER_EVENT_HASH = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; @@ -397,9 +438,8 @@ async function handle( // Instead we cover the two paths that DO reach the function and that x402scan // (@agentcash/discovery) actually uses to resolve an icon: // 1. The origin root: scrapeFavicon fetches it and parses first, - // before probing any COMMON_FAVICON_PATHS. So a GET on "/" that asks for HTML - // (a scraper, never an x402 payment client — those POST JSON) gets a minimal - // HTML page pointing at /favicon.png. + // before probing any COMMON_FAVICON_PATHS. The root now serves the agent-facing + // README page unconditionally — the favicon link stays its first head element. // 2. /favicon.png: one of scrapeFavicon's COMMON_FAVICON_PATHS fallbacks, and unlike // /favicon.ico it is not swallowed by the gateway. const normalizedPath = (event.path ?? "").replace(/^\/+/, ""); @@ -415,12 +455,13 @@ async function handle( }; } - if (isGetOrHead && normalizedPath === "" && wantsHtml(event.headers)) { + if (isGetOrHead && normalizedPath === "") { + // Unconditional on purpose: the reader is an agent, and agents send Accept: */* too. const isHead = event.httpMethod === "HEAD"; return { statusCode: 200, headers: { ...CORS_HEADERS, "Content-Type": "text/html; charset=utf-8" }, - body: isHead ? "" : FAVICON_DISCOVERY_HTML, + body: isHead ? "" : ROOT_PAGE, }; } diff --git a/scw_js/test/genimg_x402_token.test.ts b/scw_js/test/genimg_x402_token.test.ts index ee48d6217..0b7905651 100644 --- a/scw_js/test/genimg_x402_token.test.ts +++ b/scw_js/test/genimg_x402_token.test.ts @@ -637,38 +637,41 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { expect(response.body).toBe(""); }); - // x402scan fetches the origin root and parses before probing any - // favicon path. An HTML-accepting GET on "/" must return that discovery HTML. - test("should serve favicon-discovery HTML on GET / when the client accepts HTML", async () => { - const event = { - httpMethod: "GET", - headers: { accept: "text/html,application/xhtml+xml" }, - body: "", - path: "/", - }; - - const response = await handle(event, {}); - - expect(response.statusCode).toBe(200); - expect(response.headers["Content-Type"]).toContain("text/html"); - expect(response.body).toContain('rel="icon"'); - expect(response.body).toContain("/favicon.png"); - }); - - // An x402 payment client GETs / without an HTML Accept header — it must NOT get HTML, - // it must fall through to the normal "only POST" rejection. - test("should NOT serve HTML on GET / without an HTML Accept header", async () => { - const event = { - httpMethod: "GET", - headers: {}, - body: "", - path: "/", - }; + // The root page is the agent-facing README, served unconditionally — agents send + // Accept: */* too, so gating on text/html hid the page from the reader it is for. + // x402scan still gets its first. + test("should serve the agent-facing root page on GET / regardless of the Accept header", async () => { + const headerCases: Record[] = [ + {}, + { accept: "*/*" }, + { accept: "text/html,application/xhtml+xml" }, + ]; + for (const headers of headerCases) { + const event = { + httpMethod: "GET", + headers, + body: "", + path: "/", + }; - const response = await handle(event, {}); + const response = await handle(event, {}); - expect(response.statusCode).toBe(400); - expect(JSON.parse(response.body).error).toContain("Only POST"); + expect(response.statusCode).toBe(200); + expect(response.headers["Content-Type"]).toContain("text/html"); + // x402scan parses from this document — it must stay first. + expect(response.body.indexOf('rel="icon"')).toBeLessThan(response.body.indexOf("")); + expect(response.body).toContain("/favicon.png"); + expect(response.body).toContain("Fretchen AI Image Generation Service"); + expect(response.body).toContain("/openapi.json"); + expect(response.body).toContain(".well-known/agent-registration.json"); + expect(response.body).toContain(".well-known/oasf.json"); + // The page price is the spec's x-payment-info value, not a hand-restated one. + expect(response.body).toContain("POST /imagegen — $0.07 per image"); + expect(response.body).toContain( + "curl -X POST 'https://imagegen-agent.fretchen.eu/imagegen'", + ); + expect(response.body).toContain("402"); + } }); test("should reject invalid JSON body", async () => { From b13e9896b82ac69239d7ebfee1a7a74032971306 Mon Sep 17 00:00:00 2001 From: fretchen <polka.99gefiedert@icloud.com> Date: Wed, 11 Nov 2026 08:29:40 +0100 Subject: [PATCH 3/8] Delete erc-8004-ax-audit-fretchen-agents-#97599-&-#97600-2026-11-01.md --- ...tchen-agents-#97599-&-#97600-2026-11-01.md | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 bunny_js/erc-8004-ax-audit-fretchen-agents-#97599-&-#97600-2026-11-01.md diff --git a/bunny_js/erc-8004-ax-audit-fretchen-agents-#97599-&-#97600-2026-11-01.md b/bunny_js/erc-8004-ax-audit-fretchen-agents-#97599-&-#97600-2026-11-01.md deleted file mode 100644 index 9b8a9c313..000000000 --- a/bunny_js/erc-8004-ax-audit-fretchen-agents-#97599-&-#97600-2026-11-01.md +++ /dev/null @@ -1,47 +0,0 @@ -# ERC-8004 AX Audit — Fretchen Agents (Base) - -**Date:** 2026-11-01 · **Agents:** #97599 (LLM Assistant) · #97600 (Web Access) · **Registry:** `eip155:8453:0x8004a169fb4a3325136eb29fa0ceb6d2e539a432` - -## Current Scores - -| Dimension (weight) | #97599 | #97600 | -| ------------------ | --------- | ------ | -| Engagement (0.30) | 14 | 5 | -| Service (0.25) | 54 | 54 | -| Publisher (0.20) | 25 | 25 | -| Compliance (0.15) | 94 | 94 | -| Momentum (0.10) | 15 | 15 | -| **Overall** | **43.64** | **30** | - -## Key Finding - -Quality score is already **83.49** — but the leaderboard policy caps "early"-evidence agents at a **score-band ceiling of 55**. The score is limited by _evidence of usage_, not service quality. x402 support is registered (`x402_supported: true`, wallet tagged X402) but **is not a scored dimension**; per ERC-8004, payments are orthogonal — x402 only enriches feedback signals once distinct clients actually transact. - -## AX Gaps (Both Agents) - -1. **No A2A service** — `has_a2a: false`; Service score 54 is the MCP-only ceiling (`max(mcp_quality, a2a_quality)`, a2a = null). -2. **Endpoint not verified** — `is_endpoint_verified: false`. Publish `/.well-known/agent-registration.json` matching agentId + registry → verification bonus. -3. **OASF endpoint is a placeholder** — points to `github.com/agntcy/oasf/` (human page), not a machine-readable manifest. Dead end for agent clients. -4. **`supportedTrust` empty** — per spec, empty = discovery-only, no trust. Declare `["reputation", "crypto-economic"]`. -5. **Thin MCP surface** — 2 tools, 0 prompts, 0 resources; health status "unknown". -6. **Discovery metadata empty** — `tags`, `categories`, `agent_type` unset; indexer filtering cannot match capability queries. - -## Actions — #97599 (LLM Service) - -- Add A2A entry pointing to a real `/.well-known/agent-card.json` -- Fix OASF endpoint → actual manifest (hosted JSON or IPFS) -- Publish `.well-known/agent-registration.json` on fretchen.eu / MCP domain -- Set `supportedTrust`; fill tags/categories/agent_type -- Get feedback from **distinct wallets** (current: 2 ratings, both from `0x073f...7d20`, 10–11 days old → `recent_feedback_count: 0`) - -## Actions — #97600 (Web Access) - -- Same 4 registration fixes (A2A, domain verification, OASF, trust models) -- **Zero feedback** → top priority: have 3–5 distinct agent clients call it via MCP + x402 and post feedback citing payment txs -- Make per-call pricing machine-readable in metadata, not prose only - -## Sources - -- 8004scan: `8004scan.io/agents/base/97599` · `8004scan.io/agents/base/97600` · API `api.8004scan.io/api/v1/agents/8453/97599` (last scored 2026-10-31) -- ERC-8004 spec + data profiles: `eips.ethereum.org/EIPS/eip-8004` · `best-practices.8004scan.io` -- Note: API blocked automated access for #97600; its registration shape is inferred from the identical Service/Compliance/Publisher pattern — verify in dashboard. From 9b8f78f598282855ebd8dddda406f8bb9c9678b8 Mon Sep 17 00:00:00 2001 From: fretchen <polka.99gefiedert@icloud.com> Date: Wed, 11 Nov 2026 17:14:26 +0100 Subject: [PATCH 4/8] Update README.md --- bunny_js/README.md | 362 +++++++++++++-------------------------------- 1 file changed, 106 insertions(+), 256 deletions(-) diff --git a/bunny_js/README.md b/bunny_js/README.md index 52c039f89..a7463a9b8 100644 --- a/bunny_js/README.md +++ b/bunny_js/README.md @@ -1,277 +1,127 @@ # bunny_js -Bunny Edge Scripts serving the x402 sellers that outgrew `scw_js`: **searchapi** -(the `web-agent.fretchen.eu` seller — Brave `/search` + `/fetch`), **llm** (the -`llm-agent.fretchen.eu` chat seller) and **mcp** (the `mcp-agent.fretchen.eu` stateless -MCP endpoint), all paid per call/message via x402 batch-settlement. The switch is done: -these scripts are the authoritative servers behind the three `-agent.fretchen.eu` -domains, and the Scaleway twins they replaced are removed from `scw_js`'s -`serverless.yml` — the plugin deletes the functions themselves on that package's next -`npm run deploy` (run it before merging to make the docs' "retired" claims literal). -Only `scw_js`'s `llmx402cron` remains, claiming the channels these twins charge. +Four Bunny Edge Scripts, each deployed as **one bundled file**: -The fourth script is the **facilitator** (`facilitator.ts`, `service="facilitator"`) — the x402 -payment facilitator (`/verify`, `/settle`, `/supported`, `/openapi.json`) ported verbatim from -`x402_facilitator/` (payment core + its test suite) with Scaleway's envelope exchanged for -native `Request`/`Response` routing in `src/facilitator_routes.ts`. The cutover happened — -`facilitator.fretchen.eu` serves this script, and the retired Scaleway function is deleted -(the move's decisions + cutover checklist are preserved in git history as -`x402_facilitator/bunny-port-implementation-plan.md`); that package keeps only `walletreportcron` -now, the facilitator's weekly wallet-report cron — the edge has no cron). +| Script | Serves | What | +| ------------- | ------------------------- | -------------------------------------------------------------------------- | +| `searchapi` | `web-agent.fretchen.eu` | Web-search (`/search`) and single-page-fetch (`/fetch`) — paid per call | +| `llm` | `llm-agent.fretchen.eu` | Mistral chat completions (OpenAI shape) — paid per request, metered | +| `mcp` | `mcp-agent.fretchen.eu` | Stateless MCP gateway (JSON-RPC 2.0 over POST) relaying both tools' calls | +| `facilitator` | `facilitator.fretchen.eu` | The x402 facilitator (`/verify`, `/settle`, `/supported`, `/openapi.json`) | -### Facilitator: recipient gating (the "whitelist architecture") +All four are live. Three of them are **sellers** — unpaid requests get the honest `402` + +`Payment-Required` challenge; retry with the payment attached settles and returns the +result. The channel state lives in the shared S3 bucket, claimed by `scw_js`'s +`llmx402cron` (the edge has no cron), which is also why `x402_facilitator/` keeps only +`walletreportcron`. -There is no recipient whitelist. Both schemes gate recipients the same way: an allowance the -recipient has `approve()`d for the facilitator's wallet, **in the token the payment settles -in** — USDC on any network, EURC and EURe on Base. The fee is charged in that same token -(0.01 USDC, 0.01 EURC or 0.01 EURe), so a seller paid in several approves each. EURe has 18 -decimals, so its 0.01 is `10000000000000000` atomic units, not the `10000` of USDC/EURC — and -an EURe approval of `1000000` covers no fee at all. Any other token is refused with -`unsupported_fee_asset`. For `exact` the token is `requirements.asset`; for batch-settlement -it is the channel's token (`payload.token` for `settle`, the claims' channel token for -`claim`, `channelConfig.token` for a claim-carrying refund), and a batch mixing tokens is -refused. `checkMerchantAllowance()` (`src/facilitator/x402_fee.ts`) reads that allowance; an -under-approved recipient is rejected with `insufficient_fee_allowance` rather than settled -for free. `BATCH_SETTLEMENT_TEST_WALLETS` (`src/facilitator/x402_whitelist.ts`) is the one -remaining carve-out: a listed address skips the allowance check, but only on testnets (the -old manual allowlist and NFT-holder branch are retired — `claim`/`settle` moved onto the -allowance check in FEE_MODEL_PLAN.md Phase 3). +The sellers are ERC-8004-registered agents on Base. Every origin serves: -### Facilitator: the fee pull right after the settlement +- `GET /` — the **agent root page**: capability, prices, contracts and a no-wallet curl, + generated by [`src/discovery.ts`](./src/discovery.ts) from the committed spec + (nothing hand-restated). The genimg origin serves its twin from `scw_js`. +- `GET /openapi.json` — the live OpenAPI 3.1 discovery contract. +- `GET /.well-known/agent-registration.json` — the ERC-8004 registration file + (also the on-chain `agentURI`); `GET /.well-known/oasf.json` — the OASF record. -The fee is a second transaction from the facilitator wallet, sent within a second of the -settlement. On a load-balanced RPC that second send can reach a node that has not caught up: -fee pulls have reused the settlement's nonce (`replacement transaction underpriced`) and -simulated against a balance that did not yet include the payment. Two measures, both in -`src/facilitator/x402_fee.ts`: +Security posture: [`SECURITY.md`](./SECURITY.md). Agent rules: [`AGENTS.md`](./AGENTS.md). -- Every transaction is signed by an account from `loadFacilitatorAccount()`, which carries - viem's shared `nonceManager`, so the fee pull never takes a nonce below the settlement's. - Build no signer for this key any other way. -- A fee send that the node refused before broadcast (underpriced, nonce too low, or a - reverted simulation) is sent again, up to `FEE_SEND_ATTEMPTS` times, 2 s apart - (`FEE_RETRY_DELAY_MS`). Nothing that may have been broadcast is ever resent; a receipt - timeout stays `fee_collection_pending`. - -If a settlement ever hangs as `settlement_pending` on a healthy RPC after a deploy, suspect a -dropped transaction the nonce manager has already counted: a restart of the isolate clears it. - -## Architecture +## Commands -- `src/routes.ts` — the **only new code**: the routing layer, natively against web-standard - `Request`/`Response`, a line-by-line mirror of `scw_js/search_api.ts` (route table, ordering - rules, and the payment/settlement comments carried over). -- Everything else in `src/` is **ported verbatim** from `scw_js` (import paths only): - the shared seller stack (`x402_server.ts`, channel storage/sync), the services - (`search_service.ts`, `web_fetch_service.ts` with the SSRF defence), schemas, discovery, - favicon, `openapi.search.json`. Two deliberate deviations: - - `logger.ts` is a pino-shaped console shim — pino is a Node-runtime library; the lines go to - the bunny dashboard AND, since the Cockpit integration, to Scaleway's custom Loki data - source: `withLogFlush()` wraps each entry handler, so buffered lines are pushed per - request to Cockpit, where one datasource-managed alert rule on `level="error"` pages by - email (the `logger.error` convention applies again — see - [`observability/bunny-edge-script-observability-kiss-implementation-plan.md`](../observability/bunny-edge-script-observability-kiss-implementation-plan.md)). - - `utils.ts` ports only the platform-neutral half (CORS/parseJsonBody); the ScwResponse - envelope helpers are not ported — there is no Scaleway envelope here. -- `src/runtime-shims.ts` — pins the Node `Buffer`/`process` globals the ported code assumes, - imported FIRST by the entry so they exist before any ported module's top-level code runs. -- **Agent root page** — every origin serves a root page at `GET /`, unconditionally (agents - send `Accept: */*`, so a text/html gate hid the page from its reader). Built by - [`src/discovery.ts`](./src/discovery.ts)'s `buildRootPage` from what the seller already - serves — spec title/description, `x-payment-info` prices, the well-known contracts, the - no-wallet curl that returns the honest 402 — with the `<link rel="icon">` x402scan parses - kept as the first head element. Nothing on the page is hand-restated. The genimg origin - serves the same page scheme from `scw_js` (its `discovery.ts` twin). -- `src/facilitator/` — the facilitator's own corner of the tree (`src/` is otherwise the - twins'): the payment core ported **verbatim** from `x402_facilitator/` (that package keeps - its own copy alive for its Scaleway function until the teardown PR; only `walletreportcron` - stays on Scaleway long-term, because the edge has no cron) plus one new file — the routing. - `src/facilitator/facilitator_routes.ts` mirrors the Scaleway handler's routing/CORS/envelope - semantics line by line against web-standard `Request`/`Response`; everything else is - untouched payment behavior, including its test suite (`test/facilitator/` — the ported - core tests, `facilitator_routes.test.ts` for the native routing, and - `facilitator_alert_coverage.test.ts`, which pins the ported log phrases to - `observability/alerts/bunny.yaml`'s facilitator branch; the twins need no such test). - The entry is `src/facilitator.ts`, flat like the other three. +```bash +npm run build # tsdown bundle: dist/searchapi.js, dist/llm.js, dist/mcp.js, dist/facilitator.js +npm run dev:<name> # local serve of one twin via Deno on 127.0.0.1 (source, no build; --env-file=.env baked in) +npm test # vitest +npm run check # typecheck + lint + format + tests +npm run deploy # build, then `bunny scripts deploy dist/searchapi.js` +``` -## Commands +- Dev for the paid path needs `NFT_WALLET_PUBLIC_KEY` + `RECEIVER_AUTHORIZER_PRIVATE_KEY` + in `.env`; extra args go after `--`. +- Ordering (repo rule): `npm run build` in `shared/chain-utils` and `shared/s3-utils` + first — they are `file:` dependencies. -- `npm run build` — tsdown bundle to a **single** `dist/searchapi.js` (bunny deploys one file). -- `npm run dev:searchapi` / `dev:llm` / `dev:mcp` / `dev:facilitator` — local serve of one - twin via Deno on - 127.0.0.1 (from source, no build needed; `--sloppy-imports` covers the shared packages' - extensionless tsc output, and `--env-file=.env` is baked in — a missing `.env` only warns). - The paid path needs `NFT_WALLET_PUBLIC_KEY` + `RECEIVER_AUTHORIZER_PRIVATE_KEY` in `.env`; - extra args go after `--` (`npm run dev:mcp -- --port 8081`-style), because npm eats - unknown flags like `--env-file` itself. -- `npm test` — vitest (521 tests: the twins' ported suite + routes, the facilitator's ported - payment suite (`test/x402_*.test.ts`) + native routes + its alert-phrase guard; - `npm run test:integration` runs the live-RPC cases under `test/integration/`, inherited - from the facilitator package). -- `npm run check` — typecheck + lint + format + tests. -- `npm run deploy` — build, then `bunny scripts deploy dist/searchapi.js` (needs the bunny CLI, - installed and logged in). +## Architecture -Ordering (repo rule): `npm run build` in `shared/chain-utils` and `shared/s3-utils` first — -they are `file:` dependencies. +- `src/routes.ts` (searchapi), `src/llm_routes.ts`, `src/mcp_routes.ts` and + `src/facilitator/facilitator_routes.ts` are the **only native-routing code**: web-standard + `Request`/`Response` handlers that mirror the Scaleway twins' route tables and ordering + rules. +- Everything else under `src/` is **ported verbatim** from `scw_js` (the shared seller + stack, services with the SSRF defence, schemas, discovery, favicon) or from + `x402_facilitator/` (the payment core, `src/facilitator/`), with two deliberate + deviations: `logger.ts` is a pino-shaped console shim that also flushes to Scaleway + Cockpit/Loki, and `utils.ts` ports only the platform-neutral half (no Scaleway envelope). +- `src/runtime-shims.ts` — pins the Node `Buffer`/`process` globals the ported code assumes; + imported FIRST by each entry. + +### Facilitator specifics + +- **Recipient gating is an allowance gate, not a whitelist**: the recipient must have + `approve()`d the facilitator wallet for the fee **in the settled token**; under-approval + is rejected with `insufficient_fee_allowance`. `BATCH_SETTLEMENT_TEST_WALLETS` + (`src/facilitator/x402_whitelist.ts`) is the only bypass, honored on testnets only. +- **The fee pull** (`src/facilitator/x402_fee.ts`) is a second transaction right after the + settlement: it signs via viem's shared `nonceManager`, retries pre-broadcast refusals + (`FEE_SEND_ATTEMPTS` × 2 s), and never resends anything that may have been broadcast. + A settlement stuck as `settlement_pending` on a healthy RPC after a deploy means a dropped + transaction the nonce manager already counted — restarting the isolate clears it. ## Environment / secrets (bunny script env vars) -Same values the Scaleway function uses — the twin must quote the **same channel tuple** -(receiver, receiver authorizer, withdraw delay) or its channels are unusable: +Set per script via `bunny scripts env set`; `--secret` encrypts, plain stays inspectable. +Criterion: **if a value authenticates, signs, or bills, it is a secret.** -`bunny scripts env set` kinds — the criterion is not relevance but what a readable -value is worth: plain variables are stored unencrypted and readable back via -`env list`/`env pull`; `--secret` ones are encrypted and redacted. If a value -authenticates, signs, or bills it is a secret; if a public 402 response already reveals it or -it is inert without another credential, plain — and plain stays inspectable when debugging. +Sellers (`searchapi`, `llm`, `mcp` — the same channel tuple as the retired Scaleway +functions, or the channels are unusable): -| Variable | bunny kind | Use | -| ------------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `NFT_WALLET_PUBLIC_KEY` | plain | receiver/payTo (agent wallet) — a public address | -| `RECEIVER_AUTHORIZER_PRIVATE_KEY` | secret (`--secret`) | signs batch-settlement locks/refund authorizations | -| `BRAVE_API_KEY` | secret (`--secret`) | Brave LLM Context API (metered) | -| `SCW_S3_BUCKET` | plain | channel-store bucket name — NOT in `.env`; set explicitly (`my-imagestore`): the s3-utils code default points there today, but the twin must not depend on a fallback that could change | -| `SCW_S3_REGION` | plain | `nl-ams` — not in `.env` either; set explicitly alongside the bucket | -| `SCW_ACCESS_KEY` + `SCW_SECRET_KEY` | secret (`--secret`) | channel storage (the same bucket `llmx402cron` on Scaleway claims) — most sensitive pair | -| `RPC_URL_EIP155_10`, `RPC_URL_EIP155_8453` | secret (`--secret`) | viem chain reads — NOT in `.env`: production runs unset on viem's public defaults (the accepted tradeoff in scw_js's serverless.yml). Set as `--secret` only if rate limits bite — real values embed an API key | -| `FACILITATOR_URL` | plain (code default ok) | `https://facilitator.fretchen.eu` | -| `LLM_WITHDRAW_DELAY_SECONDS` | plain (code default ok) | 86400 — must match production | -| `SCW_COCKPIT_PUSH_TOKEN` | secret (`--secret`) | Cockpit push-only token — authenticates the Loki log flush to the observability data source (see the observability README/implementation plan) | -| `COCKPIT_LOKI_URL` | plain | push URL of the custom Cockpit logs data source — public and inert without the token | +| Variable | kind | Use | +| --------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | +| `NFT_WALLET_PUBLIC_KEY` | plain | receiver / payTo (public address) | +| `RECEIVER_AUTHORIZER_PRIVATE_KEY` | `--secret` | signs batch-settlement locks / refund authorizations | +| `BRAVE_API_KEY` (searchapi, mcp) | `--secret` | Brave search API (metered) | +| `MISTRAL_API_KEY` (llm, mcp) | `--secret` | upstream chat model | +| `SCW_S3_BUCKET` / `SCW_S3_REGION` | plain | channel store (`my-imagestore` / `nl-ams`; no `.env` fallback — set explicitly) | +| `SCW_ACCESS_KEY` + `SCW_SECRET_KEY` | `--secret` | channel storage on S3 | +| `RPC_URL_EIP155_10`, `RPC_URL_EIP155_8453` | `--secret` | viem chain reads; unset = viem public defaults | +| `SCW_COCKPIT_PUSH_TOKEN` / `COCKPIT_LOKI_URL` | secret / plain | Cockpit log-flush auth pair | -**Facilitator script — its own env set** (`bunny scripts env set` against its script id; the -values mirror `x402_facilitator/serverless.yml`): - -| Variable | bunny kind | Use | -| --------------------------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------- | -| `FACILITATOR_FEE_AMOUNT` | plain | fee in the token's smallest unit (`10000` = 0.01 USDC/EURC) | -| `BATCH_SETTLEMENT_TEST_WALLETS` | plain | testnet-only allowance-check bypass list (public, testnet-fenced in code) | -| `FACILITATOR_WALLET_PRIVATE_KEY` | secret (`--secret`) | the facilitator hot wallet — signs settles and pulls fees; write-once on bunny's side | -| `RPC_URL_EIP155_*` | secret (`--secret`) | the four network RPC URLs — real values embed API keys; unset falls back to rate-limited public endpoints | -| `SCW_COCKPIT_PUSH_TOKEN` / `COCKPIT_LOKI_URL` | as above | the logger pair, same as the twins | +Facilitator — its own script env (mirrors `x402_facilitator/serverless.yml`): +`FACILITATOR_FEE_AMOUNT` (plain, atomic units), `BATCH_SETTLEMENT_TEST_WALLETS` (plain), +`FACILITATOR_WALLET_PRIVATE_KEY` (`--secret`, signs settles and fee pulls), +`RPC_URL_EIP155_*` (`--secret`), plus the Cockpit pair. ## Known limits / decisions -- **Single-file bundle, 1 MB limit.** tsdown emits one `dist/searchapi.js` with `codeSplitting: -false`; Node builtins stay external and MUST be imported through the `node:` prefix - (bunny's Deno resolves them; the bundler plugin rewrites bare ones). Measured: **~397 KB**. -- **The platform rewrites Cache-Control: no-store to no-cache.** The twin sends `no-store` - on every response (a bunny_js addition: the pull zone in front is a real CDN and a cached - paid 200 would replay a purchase); bunny's edge normalizes it to `no-cache`, observed live - on 2026-10-18. Still safe — no-cache forces revalidation, and the responses carry no - validators, so every hit is an origin fetch and paid responses cannot be replayed. -- **Non-latin1 header bytes.** Web `Headers` reject the raw-JSON `X-Payment` header when the - bazaar metadata contains em-dashes/ellipses; `routes.ts` escapes non-latin1 codepoints as - JSON `\uXXXX` — lossless for any JSON-parsing client. -- **`node:dns/promises`** (SSRF defence) and `node:crypto` (SigV4) are untested on bunny's - Deno 1.x specifically — `scripts/sigv4_probe.ts` is the deployed probe for the S3 half - (local Deno: PASS). Deployed runtime verification is part of the trial matrix below. -- **50 subrequests/request** — a paid request uses: S3 ops (lock/charge) + RPC reads (channel - sync) + 1 upstream + facilitator calls. Well within the cap, but `getFacilitatorFeeConfig` - style fetch storms would not be. -- **No cron at the edge** — `llmx402cron` stays on Scaleway and claims the channels this twin - charges, unchanged, because the channel storage is the same bucket. For the same reason the - facilitator's `walletreportcron` (weekly wallet-report email) stays on Scaleway too — the - edge has no scheduler, and the report additionally rides Scaleway Transactional Email. -- **Facilitator bundle ≈ 573 KB minified** (`dist/facilitator.js`, single file, deps inlined — - viem + the @x402 SDK are the bulk), well under the 1 MB script limit. Its imports are - `node:buffer`/`node:process` only, same as the twins. - -## Trial matrix (Task 6 — historical; superseded by the production switch) - -The formal switch happened: the three `-agent.fretchen.eu` domains serve these scripts in -production, and the Scaleway twins' `serverless.yml` entries are removed from `scw_js` (the -functions are deleted by its next `npm run deploy`). The unpaid checks were run -during the trial; the paid/probe rows were overtaken by real production traffic rather than -closed as one-off exercises, so their results live in the observability logs and on-chain -settlements, not here. - -| # | Check | Result | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | -| 1 | Unpaid `GET /search` at the bunny URL → 402 + `Payment-Required` header | **PASS** 2026-10-18: 402 + Payment-Required, mainnet-only accepts, receiver 0xAAEBC1…239C, `cdn-cache: MISS` | -| 2 | `GET /openapi.json`, `/.well-known/agent-registration.json`, `/favicon.png` at the bunny URL | **PASS** 2026-10-18: 200s, correct content types and sizes (6.3 KB / 1.2 KB / 31 KB) | -| 3 | Deployed SigV4 probe (`scripts/sigv4_probe.ts` as its own script) against the real bucket | superseded by production use | -| 4 | Paid `/fetch` ($0.001) — buyer: `bunny_js/notebooks/search_x402_buyer.ipynb`, `SERVER` = "bunny-trial" with `TRIAL_URL` set → 200 + `Payment-Response` | superseded by production use | -| 5 | Paid `/search` ($0.01) via the same notebook → 200 + results | superseded by production use | -| 6 | Channel record in S3 updated; next `llmx402cron` run claims it cleanly (observability logs) | superseded by production use | -| 7 | Cold-start spot checks at the bunny URL | superseded by production use | - -## Status - -Ported and production. The three twins serve the llm, web and MCP sellers at their -`*.fretchen.eu` origins; `scw_js` deploys only `genimgx402token`, `llmx402cron` (which claims -these twins' channels) and `growthapi` — the old twins' entries are gone from its -`serverless.yml`, and its next `npm run deploy` deletes the retired functions from Scaleway. -The original port plan (scaffold, bundle gate, SigV4 spike, verbatim stack + tests, native -routing + tests) is complete. - -The facilitator twin is ported and green but **not yet live**: `facilitator.fretchen.eu` still -serves the Scaleway function until the port plan's operational cutover (trial-hostname probes -→ DNS flip), after which `x402_facilitator/` trims down to `walletreportcron` in the teardown -PR. - -## Deploy paths (both can coexist) - -Bunny publishes whichever deployment happened **last**, so a manual CLI deploy and the -GitHub Action interoperate by design — deploy from the laptop for an experiment, and the -next push to `main` overwrites it back to the committed state. - -1. **Manual (CLI):** `npm run deploy` (= build + `bunny scripts deploy dist/searchapi.js`), - needs the bunny CLI — `curl -fsSL https://cli.bunny.net/install.sh | sh` (single binary, - independent of the nvm-managed Node that a global npm install would bind it to) — then - `bunny login`. The llm and mcp twins deploy by naming their bundle and script id - explicitly: `bunny scripts deploy dist/llm.js <LLM_SCRIPT_ID>` and - `bunny scripts deploy dist/mcp.js <MCP_SCRIPT_ID>`. -2. **GitHub Actions:** `.github/workflows/deploy-bunny-js.yml` — on push to `main` touching - `bunny_js/**` or the shared libs, builds (with the repo's shared-deps-first ordering) and - publishes all three bundles via the official `BunnyWay/actions/deploy-script` action: - `dist/searchapi.js`, `dist/llm.js`, `dist/mcp.js`. Requires two repo secrets per script, - both from the bunny dashboard (**Script -> Deployments -> Settings**): - - `BUNNYJS_SCRIPT_ID` / `BUNNYJS_DEPLOY_KEY` — the searchapi script - - `BUNNYLLM_SCRIPT_ID` / `BUNNYLLM_DEPLOY_KEY` — the llm script - - `BUNNYMCP_SCRIPT_ID` / `BUNNYMCP_DEPLOY_KEY` — the mcp script - - `BUNNYFACILITATOR_SCRIPT_ID` / `BUNNYFACILITATOR_DEPLOY_KEY` — the facilitator script - (same pattern; serves `facilitator.fretchen.eu` after its DNS flip — see the port plan) - - (Per-script deploy keys are deliberately chosen over the account-wide API key a - CLI-in-CI variant would need — narrower blast radius. A step whose secrets are missing - fails; the searchapi step runs first, so its deploy is never blocked by the others.) - -The workflow ships code only; the script's environment variables and secrets are configured -on the script itself (dashboard or `bunny scripts env set`), so a repo-side leak cannot leak -the Brave key, the authorizer key, or the S3 channel-store credentials. - -## First deploy (one-time) - -`npm run deploy` alone fails the first time — it pushes to a remote script that must exist, -and the `.bunny/script.json` link comes out of the create step. One-time sequence: - -1. `bunny scripts create --type standalone` — creates the remote script and links this - directory (writes `.bunny/script.json`, gitignored — machine-local, like the docs treat it). - For the SECOND and THIRD script, add `--no-link` — the directory's link belongs to - searchapi, and `create` without it would silently repoint `.bunny/script.json`; the - other twins take their id from the create output (or `bunny scripts list`) instead. -2. Upload the env from `.env` with `bunny scripts env set`, marking the sensitive keys with - `--secret` (RECEIVER_AUTHORIZER_PRIVATE_KEY, BRAVE_API_KEY, MISTRAL_API_KEY, - SCW_ACCESS_KEY, SCW_SECRET_KEY, RPC_URL_*, SCW_COCKPIT_PUSH_TOKEN). Skip - `TEST_WALLET_PRIVATE_KEY` — buyer-side only, the server must never see it. Verify with - `bunny scripts env list`. Per script — all three also get the Cockpit observability pair - (`SCW_COCKPIT_PUSH_TOKEN` as `--secret`, `COCKPIT_LOKI_URL` plain): - - **searchapi** — the channel stack + `BRAVE_API_KEY` (web_search/fetch) + `SCW_S3_*`. - - **llm** — the channel stack + `MISTRAL_API_KEY` (chat_completion) + `SCW_S3_*`. - - **mcp** — the union: both `BRAVE_API_KEY` and `MISTRAL_API_KEY` (its bundle embeds - both tools) + `SCW_S3_*`. -3. `npm run deploy` (searchapi) / `bunny scripts deploy dist/llm.js <id>` / - `bunny scripts deploy dist/mcp.js <id>` — publishes each; the output prints the live - `*.bunny.run` URL. -4. Unpaid smoke: `curl -D - 'https://<that-url>.bunny.run/search?q=x402'` → 402 + - `Payment-Required` header. The 402's `resource.url` keeps naming the production origin - (it is derived from the committed spec, like the llm twin) — the resource url is the - payment scope, not the serving origin, so a trial does not change it. -5. For the GitHub Action: `bunny scripts show` (or `list`) gives each script id; the deploy - keys are in the dashboard (Script -> Deployments -> Settings). Add each pair as the - `BUNNYJS_*` / `BUNNYLLM_*` / `BUNNYMCP_*` repo secrets. - -After the first deploy, the Action and manual `npm run deploy` are interchangeable — last -deployment wins. +- **Bunny's runtime is Deno 1.x, not Node.** Assumes only what `src/runtime-shims.ts` pins; + Node builtins must be imported through the `node:` prefix. +- **1 MB single-file bundle limit.** tsdown emits one file per script (`codeSplitting: +false`); measured well under the cap (run `npm run build` to check). +- **The CDN in front rewrites `no-store` to `no-cache`.** Still safe: the responses carry no + validators, so every hit goes to the origin and paid responses cannot be replayed. +- **Non-latin1 header bytes** are escaped as JSON `\uXXXX` in payment headers — lossless + for any JSON-parsing client. +- **50 subrequests per request** — comfortably within budget for one paid request; fetch + storms are not. +- **No cron at the edge** — `llmx402cron` (`scw_js`) claims these channels; + `walletreportcron` (`x402_facilitator/`) sends the weekly wallet report. + +## Deploying + +Bunny publishes whichever deployment happened **last** — CLI and Actions interoperate, and +the next push to `main` restores the committed state. + +1. **GitHub Actions** (preferred): `.github/workflows/deploy-bunny-js.yml` builds on push to + `main` touching `bunny_js/**` or the shared libs and publishes all four bundles. Needs + the per-script secrets `BUNNYJS_*`, `BUNNYLLM_*`, `BUNNYMCP_*`, `BUNNYFACILITATOR_*` + (`<NAME>_SCRIPT_ID` + `<NAME>_DEPLOY_KEY`, from the dashboard: Script → Deployments → + Settings). The workflow ships code only — env vars and secrets live on the script itself. +2. **Manual**: `npm run deploy` for searchapi; the others by id: + `bunny scripts deploy dist/llm.js <id>` etc. + +First-time setup (once, not in CI): `bunny scripts create --type standalone` creates each +script (only searchapi takes the `.bunny/script.json` link; the others use `--no-link`), +then `bunny scripts env set` per the tables above, then the smoke test: +`curl -D - 'https://<origin>/search?q=x402'` → 402 + `Payment-Required` header. + +The trial matrix and facilitator-cutover checklist of the port are preserved in git history. From 3aff06c52894a222a5644b5597227830cdaa5092 Mon Sep 17 00:00:00 2001 From: fretchen <polka.99gefiedert@icloud.com> Date: Wed, 11 Nov 2026 21:22:06 +0100 Subject: [PATCH 5/8] sellers: exact-scheme alternative on web-agent and llm-agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 402 challenge now lists two schemes: the batch-settlement channel entries first (unchanged prices and behavior), plus ONE appended exact alternative — a single EIP-3009 signature per call, flat 10000 atomic ($0.01) in USDC on Base mainnet — so stock x402 clients without channel wiring can pay per call. Batch prices are untouched; the two schemes price independently (which is why /fetch quotes $0.01 under exact despite its $0.001 channel price: the flat price equals the facilitator's per-settle fee, so a settled exact call nets zero minus upstream cost — accepted break-even for the simplicity). - x402_server.ts: createExactAlternative (the single accepts entry, also the per-call verify/settle requirements; USDC-on-Base resolved from the chain-utils registry) + withExactAlternative (appends the entry after the batch accepts — the order is the contract). - searchapi_routes/llm_routes: challenge wrapped; paid path forks on accepted.scheme. The exact flow verifies/settles through the plain exact resource server with no channel machinery (no enhance, no locks, no releaseLock), answers invalid payments with a plain 402 instead of the SDK corrective, keeps upstream-failure-free semantics (settle only after a successful run), and settles the same flat amount it verified (no usage metering on the llm side). - MCP inherits both schemes unchanged (it relays headers to these handlers); tests pin the exact entry's shape and appended-last position, the channel-free call path, the flat settle, the never-offered-terms rejection, and the untouched batch behavior. Generated docs follow: both root pages' payment lines and both specs' x-guidance name the alternative (generated-where-possible); README describes the dual-scheme challenge. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> --- bunny_js/README.md | 10 +- bunny_js/src/llm_routes.ts | 208 +++++++++++++++++++----------- bunny_js/src/openapi.llm.json | 2 +- bunny_js/src/openapi.search.json | 2 +- bunny_js/src/searchapi_routes.ts | 130 ++++++++++++++++++- bunny_js/src/x402_server.ts | 57 ++++++++ bunny_js/test/llm.test.ts | 203 +++++++++++++++++++++++++---- bunny_js/test/routes.test.ts | 215 +++++++++++++++++++++++++++++-- 8 files changed, 712 insertions(+), 115 deletions(-) diff --git a/bunny_js/README.md b/bunny_js/README.md index a7463a9b8..a3aa464b1 100644 --- a/bunny_js/README.md +++ b/bunny_js/README.md @@ -11,9 +11,13 @@ Four Bunny Edge Scripts, each deployed as **one bundled file**: All four are live. Three of them are **sellers** — unpaid requests get the honest `402` + `Payment-Required` challenge; retry with the payment attached settles and returns the -result. The channel state lives in the shared S3 bucket, claimed by `scw_js`'s -`llmx402cron` (the edge has no cron), which is also why `x402_facilitator/` keeps only -`walletreportcron`. +result. The sellers' 402 lists **two schemes**: batch-settlement channel entries first +(the preferred way to pay), plus an appended **exact alternative** — one EIP-3009 signature +per call, flat $0.01 USDC on Base mainnet — for stock clients without channel wiring; the +flat price is decoupled from the channel prices (see `withExactAlternative` in +[`src/x402_server.ts`](./src/x402_server.ts) for the fee rationale). The channel state lives +in the shared S3 bucket, claimed by `scw_js`'s `llmx402cron` (the edge has no cron), which is +also why `x402_facilitator/` keeps only `walletreportcron`. The sellers are ERC-8004-registered agents on Base. Every origin serves: diff --git a/bunny_js/src/llm_routes.ts b/bunny_js/src/llm_routes.ts index cc56c0624..0a26454bd 100644 --- a/bunny_js/src/llm_routes.ts +++ b/bunny_js/src/llm_routes.ts @@ -36,20 +36,25 @@ import { MAX_PRICE_ATOMIC, getSettleAmount, } from "./llm_schemas.js"; -import { isTestnet } from "@fretchen/chain-utils"; +import { isTestnet, type SettlementTokenInfo } from "@fretchen/chain-utils"; import { logger } from "./logger.js"; import { offeredStablecoins, resolvePaidStablecoin } from "./stablecoin_pricing.js"; import { toResponse } from "./response.js"; import { createLLMResourceServer, + EXACT_ALTERNATIVE_AMOUNT_ATOMIC, + EXACT_ALTERNATIVE_NETWORK, createBatchSettlementPaymentRequirements, create402Response, + createExactAlternative, + createResourceServer, extractPaymentPayload, createSettlementHeaders, getBatchSettlementNetworks, LLM_MAX_TIMEOUT_SECONDS, formatUsdcAtomicAsDecimalUsd, releaseLock, + withExactAlternative, type SdkPaymentPayload, type SdkPaymentRequirements, } from "./x402_server.js"; @@ -105,8 +110,7 @@ const ROOT_PAGE = buildRootPage({ price: [ `POST / — metered by real token usage, at most $${formatUsdcAtomicAsDecimalUsd(MAX_PRICE_ATOMIC.USDC)} per request`, ], - payment: - "x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — exact terms (scheme batch-settlement, network, token, ceiling) are in the challenge, then retry the same request with the payment attached.", + payment: `x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — two schemes are listed: batch-settlement channels first (metered by real token usage under a per-request ceiling), and an exact alternative at the end for clients without channel wiring — one EIP-3009 signature per call, flat $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} in USDC on Base mainnet. Retry the same request with the payment attached.`, tryIt: `curl -X POST '${openapiSpec.servers[0].url}/' -H 'Content-Type: application/json' -d '{"model":"${advertisedModelIds()[0]}","messages":[{"role":"user","content":"ping"}]}'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.\n# Testnet note: on Base Sepolia the payment settles and the completion is a mocked answer — a free end-to-end test.`, errors: "402 = unpaid or invalid payment (challenge in the Payment-Required header); 400 = invalid request (OpenAI-shaped error body); 404 = unknown model. Response schemas: openapi.json.", @@ -249,7 +253,12 @@ export async function handleLLMRequest(request: Request): Promise<Response> { tags: ["llm", "chat", "x402"], iconUrl: `${openapiSpec.servers[0].url}/favicon.png`, }); - return toResponse(create402Response(paymentRequirements)); + // Append the exact alternative AFTER the batch entries — order is the contract (see + // withExactAlternative): channel buyers keep their preferred entries, channel-less + // clients fall through to the exact one. + return toResponse( + create402Response(withExactAlternative(paymentRequirements, receiverAddressForChallenge)), + ); } // OpenAI chat-completions request body — everything we do not consume is FORWARDED to the @@ -345,74 +354,115 @@ export async function handleLLMRequest(request: Request): Promise<Response> { ); } - let resourceServer: ReturnType<typeof createLLMResourceServer>["resourceServer"]; - let scheme: ReturnType<typeof createLLMResourceServer>["scheme"]; - try { - ({ resourceServer, scheme } = createLLMResourceServer(receiverAddress)); - } catch (err) { - logger.error({ err }, "Failed to configure batch-settlement resource server"); - return jsonError(500, "Server configuration error"); - } - // Non-null: the unpaid case already returned the 402 challenge above, before validation. const paymentPayload = paymentPayloadEarly; + const accepted = (paymentPayload as Record<string, unknown>)["accepted"] as + Record<string, unknown> | undefined; + const clientNetwork = accepted?.["network"] as string | undefined; - const clientNetwork = (paymentPayload as Record<string, unknown>)["accepted"] - ? (((paymentPayload as Record<string, unknown>)["accepted"] as Record<string, unknown>)[ - "network" - ] as string | undefined) - : undefined; + // ─── Scheme fork ──────────────────────────────────────────────────────────────────── + // The 402 lists the batch-settlement entries first and the exact alternative last (see + // withExactAlternative); the payment names which one it pays against. The exact path is + // channel-free — no enhancePaymentRequirements, no channel lock — which is also why an + // invalid exact payment answers a plain 402 below instead of the batch path's corrective + // resync (there is no channel state to resync). + const isExact = accepted?.["scheme"] === "exact"; - if (!clientNetwork || !getBatchSettlementNetworks().includes(clientNetwork)) { - return jsonError(402, "Unsupported or missing network for batch-settlement payment"); - } + let resourceServer: ReturnType<typeof createLLMResourceServer>["resourceServer"]; + let paymentRequirements: SdkPaymentRequirements; + let useMock: boolean; + let coin: SettlementTokenInfo; + + if (isExact) { + // The exact entry was offered on exactly one (network, token); anything else is a + // caller paying against terms we never issued. + const exactRequirements = createExactAlternative(receiverAddress); + if ( + clientNetwork !== EXACT_ALTERNATIVE_NETWORK || + accepted?.["asset"] !== exactRequirements.asset + ) { + return jsonError( + 402, + `Unsupported exact payment terms. This endpoint's exact scheme accepts ${EXACT_ALTERNATIVE_NETWORK} and ${exactRequirements.asset} only`, + ); + } + // Mainnet-only offer, so the testnet mock branch cannot apply; only an explicit + // useDummyData still selects the stand-in. + useMock = useDummyData === true; + // Guaranteed by the terms check above (the asset IS ours), but stay symmetric with the + // batch branch rather than trust an invariant across two modules. + const resolvedCoin = resolvePaidStablecoin(EXACT_ALTERNATIVE_NETWORK, exactRequirements.asset); + if (!resolvedCoin) { + logger.error( + { network: EXACT_ALTERNATIVE_NETWORK, asset: exactRequirements.asset }, + "Config error: exact-alternative asset not offered", + ); + return jsonError(500, "Server configuration error"); + } + coin = resolvedCoin; + resourceServer = createResourceServer(); + paymentRequirements = exactRequirements; + } else { + let scheme: ReturnType<typeof createLLMResourceServer>["scheme"]; + try { + ({ resourceServer, scheme } = createLLMResourceServer(receiverAddress)); + } catch (err) { + logger.error({ err }, "Failed to configure batch-settlement resource server"); + return jsonError(500, "Server configuration error"); + } - // SECURITY: testnet must never reach the real Mistral API (scw parity). An explicit - // `useDummyData: false` on a testnet network is always a caller error — reject loudly. - if (isTestnet(clientNetwork) && useDummyData === false) { - return jsonError( - 400, - "Real inference is not available on testnet — this network always returns a mocked LLM response. Omit useDummyData or set it to true, or use a mainnet network for a real completion.", - ); - } - // Never spend real inference budget on a valueless testnet payment (scw parity). - const useMock = useDummyData === true || isTestnet(clientNetwork); + if (!clientNetwork || !getBatchSettlementNetworks().includes(clientNetwork)) { + return jsonError(402, "Unsupported or missing network for batch-settlement payment"); + } - // Price the stablecoin the buyer's channel is in, not a fixed one (scw parity). - const accepted = (paymentPayload as Record<string, unknown>)["accepted"] as - Record<string, unknown> | undefined; - const coin = resolvePaidStablecoin(clientNetwork, accepted?.["asset"]); - if (!coin) { - logger.warn({ clientNetwork, asset: accepted?.["asset"] }, "Payment asset not offered"); - return jsonError( - 402, - `Unsupported payment asset on ${clientNetwork} — offered: ${offeredStablecoins(clientNetwork) - .map((offered) => `${offered.symbol} ${offered.address}`) - .join(", ")}`, - ); - } - const baseRequirements: SdkPaymentRequirements = { - scheme: "batch-settlement", - network: clientNetwork as `${string}:${string}`, - amount: MAX_PRICE_ATOMIC[coin.symbol], - asset: coin.address, - payTo: receiverAddress, - maxTimeoutSeconds: LLM_MAX_TIMEOUT_SECONDS, - extra: { name: coin.name, version: coin.version }, - }; - // Must be the SAME enhanced requirements the client signed its channelConfig against — - // a raw object here makes the facilitator reject every deposit with - // receiver_authorizer_mismatch (scw parity; confirmed via a real Base Sepolia run). - const paymentRequirements = await scheme.enhancePaymentRequirements( - baseRequirements, - { - x402Version: 2, + // SECURITY: testnet must never reach the real Mistral API (scw parity). An explicit + // `useDummyData: false` on a testnet network is always a caller error — reject loudly. + if (isTestnet(clientNetwork) && useDummyData === false) { + return jsonError( + 400, + "Real inference is not available on testnet — this network always returns a mocked LLM response. Omit useDummyData or set it to true, or use a mainnet network for a real completion.", + ); + } + // Never spend real inference budget on a valueless testnet payment (scw parity). + useMock = useDummyData === true || isTestnet(clientNetwork); + + // Price the stablecoin the buyer's channel is in, not a fixed one (scw parity). + const resolvedCoin = resolvePaidStablecoin(clientNetwork, accepted?.["asset"]); + if (!resolvedCoin) { + logger.warn({ clientNetwork, asset: accepted?.["asset"] }, "Payment asset not offered"); + return jsonError( + 402, + `Unsupported payment asset on ${clientNetwork} — offered: ${offeredStablecoins( + clientNetwork, + ) + .map((offered) => `${offered.symbol} ${offered.address}`) + .join(", ")}`, + ); + } + coin = resolvedCoin; + const baseRequirements: SdkPaymentRequirements = { scheme: "batch-settlement", network: clientNetwork as `${string}:${string}`, - extra: baseRequirements.extra, - }, - [], - ); + amount: MAX_PRICE_ATOMIC[coin.symbol], + asset: coin.address, + payTo: receiverAddress, + maxTimeoutSeconds: LLM_MAX_TIMEOUT_SECONDS, + extra: { name: coin.name, version: coin.version }, + }; + // Must be the SAME enhanced requirements the client signed its channelConfig against — + // a raw object here makes the facilitator reject every deposit with + // receiver_authorizer_mismatch (scw parity; confirmed via a real Base Sepolia run). + paymentRequirements = await scheme.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "batch-settlement", + network: clientNetwork as `${string}:${string}`, + extra: baseRequirements.extra, + }, + [], + ); + } let verification: { isValid: boolean; @@ -431,13 +481,21 @@ export async function handleLLMRequest(request: Request): Promise<Response> { } if (!verification.isValid) { - // Through createPaymentRequiredResponse (not a hand-rolled body) so the SDK's corrective - // enrichment runs — channelState/voucherState for cumulative mismatches, which the client - // SDK's processCorrectivePaymentRequired needs to resync and retry (scw parity). logger.warn( { reason: verification.invalidReason, message: verification.invalidMessage }, "Payment verification failed", ); + if (isExact) { + // Plain 402, not the batch corrective re-challenge: exact holds no channel state to + // resync — the client retries with a fresh signature against the challenge it has. + return jsonError( + 402, + `Payment verification failed: ${verification.invalidReason ?? "unknown"}`, + ); + } + // Through createPaymentRequiredResponse (not a hand-rolled body) so the SDK's corrective + // enrichment runs — channelState/voucherState for cumulative mismatches, which the client + // SDK's processCorrectivePaymentRequired needs to resync and retry (scw parity). const paymentRequired = await resourceServer.createPaymentRequiredResponse( [paymentRequirements], { @@ -485,16 +543,22 @@ export async function handleLLMRequest(request: Request): Promise<Response> { logger.error({ err: error }, "Error during answer generation"); // Verified but never settled: release the channel lock, or the next message waits out // LLM_MAX_TIMEOUT_SECONDS (the lock TTL) behind `channel_busy` for an answer that was - // never charged. - await releaseLock(resourceServer, paymentPayload as SdkPaymentPayload, paymentRequirements); + // never charged. Exact holds no lock, so there is nothing to release. + if (!isExact) { + await releaseLock(resourceServer, paymentPayload as SdkPaymentPayload, paymentRequirements); + } return jsonError(500, (error as Error).message); } // Settling with a DIFFERENT (smaller, real-usage) amount than verifyPayment() used is // intentional (scw parity — see getSettleAmount() in llm_schemas.ts): handleBeforeSettle - // only checks against the voucher's signed ceiling, not against what verify saw. - const settleAmount = getSettleAmount(llmData.usage, coin.symbol); - const settleRequirements = { ...paymentRequirements, amount: settleAmount }; + // only checks against the voucher's signed ceiling, not against what verify saw. The + // exact alternative has no ceiling concept — its price is the flat per-call amount, so + // the settle amount equals the verified one. + const settleRequirements = isExact + ? paymentRequirements + : { ...paymentRequirements, amount: getSettleAmount(llmData.usage, coin.symbol) }; + const settleAmount = settleRequirements.amount; try { const settlement = await resourceServer.settlePayment( diff --git a/bunny_js/src/openapi.llm.json b/bunny_js/src/openapi.llm.json index 6dc1b498b..5d2b27000 100644 --- a/bunny_js/src/openapi.llm.json +++ b/bunny_js/src/openapi.llm.json @@ -4,7 +4,7 @@ "title": "Fretchen LLM Inference — pay-per-call chat completions for agents", "description": "OpenAI-shaped chat completions from Mistral models, sold per request to autonomous agents via x402 batch-settlement payment channels — no account, no API key. Metered by real token usage under a fixed per-request ceiling; settled in USDC (Base or Optimism), EURC or EURe (Base), while Base Sepolia settles for free and answers with a mocked completion.", "version": "1.0.0", - "x-guidance": "OpenAI chat-completions body. POST / with { model, messages: [{ role, content }, ...] } and no payment header to receive a 402 with x402 batch-settlement payment requirements (accepts[]). Open/top up a payment channel per the requirements, retry with the payment header, and the service returns a standard OpenAI chat.completion object. Streaming (stream: true) is not supported. Each message is metered and settled up to a per-message price ceiling; the real cost is usage-derived and typically lower. Other standard OpenAI chat params (temperature, top_p, stop, seed, the penalties, response_format) are forwarded to the upstream model and work as normal; they are not enumerated here because the upstream owns that contract. The exceptions are stream, n and max_tokens, which are rejected with a 400 rather than ignored, because each would move cost past the fixed per-message ceiling this endpoint meters against. Tool calling is supported (see x-capabilities): send OpenAI-shaped tools, and a tool call comes back as choices[].message.tool_calls with finish_reason: \"tool_calls\" and content: null. Execute it yourself and send the result back as a role:\"tool\" message with the matching tool_call_id — this endpoint never calls a tool on your behalf, and accepts no callback or MCP server URL. Note: payment uses x402 batch-settlement (a stateful channel), so a stock OpenAI SDK cannot pay this endpoint without batch-settlement client wiring — the OpenAI shape is for body legibility, not drop-in SDK use.", + "x-guidance": "OpenAI chat-completions body. POST / with { model, messages: [{ role, content }, ...] } and no payment header to receive a 402 with x402 batch-settlement payment requirements (accepts[]). Open/top up a payment channel per the requirements, retry with the payment header, and the service returns a standard OpenAI chat.completion object. Streaming (stream: true) is not supported. Each message is metered and settled up to a per-message price ceiling; the real cost is usage-derived and typically lower. Other standard OpenAI chat params (temperature, top_p, stop, seed, the penalties, response_format) are forwarded to the upstream model and work as normal; they are not enumerated here because the upstream owns that contract. The exceptions are stream, n and max_tokens, which are rejected with a 400 rather than ignored, because each would move cost past the fixed per-message ceiling this endpoint meters against. Tool calling is supported (see x-capabilities): send OpenAI-shaped tools, and a tool call comes back as choices[].message.tool_calls with finish_reason: \"tool_calls\" and content: null. Execute it yourself and send the result back as a role:\"tool\" message with the matching tool_call_id — this endpoint never calls a tool on your behalf, and accepts no callback or MCP server URL. Note: payment uses x402 batch-settlement (a stateful channel), so a stock OpenAI SDK cannot pay this endpoint without batch-settlement client wiring — the OpenAI shape is for body legibility, not drop-in SDK use. After the channel entries, the 402 also carries one exact-scheme alternative: a single EIP-3009 signature per call, flat 0.01 USDC on Base mainnet, unmetered — the usage-derived settlement and the per-message ceiling apply to channel payments only — for clients that cannot open channels at all. The live challenge is the source of truth for its terms.", "contact": { "name": "fretchen", "url": "https://www.fretchen.eu", diff --git a/bunny_js/src/openapi.search.json b/bunny_js/src/openapi.search.json index fe2f87558..6fd5443a0 100644 --- a/bunny_js/src/openapi.search.json +++ b/bunny_js/src/openapi.search.json @@ -4,7 +4,7 @@ "title": "Fretchen Web Retrieval — pay-per-call web access for agents", "description": "Web search and single-page fetch for agents, paid per call via x402 batch-settlement payment channels in USDC (or EURC on Base).", "version": "1.0.0", - "x-guidance": "GET /search?q=... or GET /fetch?url=... with no payment header to receive a 402 carrying x402 batch-settlement payment requirements (accepts[]). Open or top up a payment channel per those requirements, retry with the payment header, and the service returns JSON. Payment travels in the header only — these are GET routes, so there is no body fallback. Both routes bill onto the SAME channel as llm-agent.fretchen.eu when the payer, voucher signer and receiver match, so an agent already chatting there pays for web access without a second deposit. Prices are fixed per route and settled only when the call succeeds: an upstream failure returns 500 and takes no money. Mainnet only — testnet USDC is free and these routes spend real money on the caller's behalf.", + "x-guidance": "GET /search?q=... or GET /fetch?url=... with no payment header to receive a 402 carrying x402 batch-settlement payment requirements (accepts[]). Open or top up a payment channel per those requirements, retry with the payment header, and the service returns JSON. Payment travels in the header only — these are GET routes, so there is no body fallback. Both routes bill onto the SAME channel as llm-agent.fretchen.eu when the payer, voucher signer and receiver match, so an agent already chatting there pays for web access without a second deposit. Prices are fixed per route and settled only when the call succeeds: an upstream failure returns 500 and takes no money. Mainnet only — testnet USDC is free and these routes spend real money on the caller's behalf. After the channel entries, the 402 also carries one exact-scheme alternative: a single EIP-3009 signature per call, flat 0.01 USDC on Base mainnet, priced independently of the channel prices — for clients that cannot open batch-settlement channels. The live challenge is the source of truth for its terms.", "contact": { "name": "fretchen", "url": "https://www.fretchen.eu", diff --git a/bunny_js/src/searchapi_routes.ts b/bunny_js/src/searchapi_routes.ts index d311f1827..df45bdf00 100644 --- a/bunny_js/src/searchapi_routes.ts +++ b/bunny_js/src/searchapi_routes.ts @@ -37,12 +37,18 @@ import openapiSpec from "./openapi.search.json" with { type: "json" }; import { buildSearchBazaarExtension } from "./bazaar_discovery.js"; import { toResponse } from "./response.js"; import { + EXACT_ALTERNATIVE_AMOUNT_ATOMIC, + EXACT_ALTERNATIVE_NETWORK, create402Response, createBatchSettlementPaymentRequirements, + createExactAlternative, createLLMResourceServer, + createResourceServer, createSettlementHeaders, extractPaymentPayload, + formatUsdcAtomicAsDecimalUsd, releaseLock, + withExactAlternative, type SdkPaymentPayload, type SdkPaymentRequirements, } from "./x402_server.js"; @@ -111,8 +117,7 @@ const ROOT_PAGE = buildRootPage({ )[`/${route}`].get["x-payment-info"]; return `GET /${route} — $${info?.price.amount ?? "?"} per call`; }), - payment: - "x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — exact terms (scheme batch-settlement, network, token, amount) are in the challenge, then retry the same request with the payment attached.", + payment: `x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — two schemes are listed: batch-settlement channels first (the preferred way to pay), and an exact alternative at the end for clients without channel wiring — one EIP-3009 signature per call, flat $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} in USDC on Base mainnet. Retry the same request with the payment attached.`, tryIt: `curl '${SERVICE_URL}/search?q=erc-8004'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.`, errors: "402 = unpaid (challenge in the Payment-Required header); 404 = no such route; everything else is standard HTTP. Response schemas: openapi.json.", @@ -191,8 +196,17 @@ function acceptedNetwork(payload: Record<string, unknown>): string | null { return typeof network === "string" ? network : null; } +/** The scheme the payer built their payload against, or null if they named none. */ +function acceptedScheme(payload: Record<string, unknown>): string | null { + const accepted = payload["accepted"] as Record<string, unknown> | undefined; + const scheme = accepted?.["scheme"]; + return typeof scheme === "string" ? scheme : null; +} + /** * Serve one route against a payment: verify, run, and settle **only if the run succeeded**. + * The batch-settlement path below is the original; payments carrying the exact alternative + * (the last accepts entry of the 402, see x402_server.ts) take their own, channel-free flow. * * Settle-after-success is what makes an upstream failure free for the caller. It leaves a * verify without a matching settle, which holds the channel lock for MAX_TIMEOUT_SECONDS — @@ -203,6 +217,113 @@ async function servePaid( args: Args, payload: Record<string, unknown>, receiverAddress: `0x${string}`, +): Promise<Response> { + if (acceptedScheme(payload) === "exact") { + return serveExact(route, args, payload, receiverAddress); + } + return serveBatchSettlement(route, args, payload, receiverAddress); +} + +/** + * The exact alternative's paid flow: one EIP-3009 signature per call, Base mainnet USDC, + * flat EXACT_ALTERNATIVE_AMOUNT_ATOMIC (see x402_server.ts for why the price is what it is). + * Deliberately resembles the batch flow (verify → run → settle-on-success → Payment-Response) + * while touching NONE of the channel machinery: no enhancePaymentRequirements, no channel + * lock, no releaseLock — there is no state for any of it to act on. + */ +async function serveExact( + route: Route, + args: Args, + payload: Record<string, unknown>, + receiverAddress: `0x${string}`, +): Promise<Response> { + // The exact entry was offered on exactly one network/token; anything else is the caller + // paying against terms we never issued. + const network = acceptedNetwork(payload); + const paidAsset = (payload["accepted"] as Record<string, unknown> | undefined)?.["asset"]; + const exactRequirements = createExactAlternative(receiverAddress); + if (network !== EXACT_ALTERNATIVE_NETWORK || paidAsset !== exactRequirements.asset) { + return jsonResponse(402, { + error: `Unsupported exact payment terms. This endpoint's exact scheme accepts ${ + EXACT_ALTERNATIVE_NETWORK + } and ${exactRequirements.asset} only`, + }); + } + + const resourceServer = createResourceServer(); + + let verification: { + isValid: boolean; + invalidReason?: string; + invalidMessage?: string; + payer?: string; + }; + try { + verification = await resourceServer.verifyPayment( + payload as SdkPaymentPayload, + exactRequirements, + ); + } catch (err) { + logger.error({ err }, "Payment verification error"); + return jsonResponse(402, { error: `Payment verification failed: ${(err as Error).message}` }); + } + + if (!verification.isValid) { + logger.warn( + { reason: verification.invalidReason, message: verification.invalidMessage }, + "Payment verification failed", + ); + // A plain 402, not the batch path's corrective re-challenge: exact has no channel state + // to resync — the client retries with a fresh signature against the challenge it has. + return jsonResponse(402, { + error: `Payment verification failed: ${verification.invalidReason ?? "unknown"}`, + ...(verification.payer ? { payer: verification.payer } : {}), + }); + } + + let data: unknown; + try { + data = await run(args); + } catch (err) { + // Nothing settled and nothing to release — exact holds no locks. + logger.warn({ err, route }, "Run failed after exact verify"); + return errorResponseFor(err); + } + + let settlement: Record<string, unknown> & { success: boolean; errorReason?: string }; + try { + settlement = await resourceServer.settlePayment( + payload as SdkPaymentPayload, + exactRequirements, + ); + } catch (err) { + logger.error({ err }, "Settlement error"); + return jsonResponse(402, { error: `Settlement failed: ${(err as Error).message}` }); + } + + if (!settlement.success) { + logger.error({ settlement }, "Settlement failed"); + return jsonResponse(402, { + error: `Settlement failed: ${settlement.errorReason ?? "unknown"}`, + }); + } + + logger.info( + { route, network, scheme: "exact", amount: exactRequirements.amount }, + "Served and settled", + ); + return new Response(JSON.stringify(data), { + status: 200, + headers: { ...CORS_HEADERS, ...createSettlementHeaders(settlement) }, + }); +} + +/** The original batch-settlement flow (channels, vouchers, shared locks with the chat). */ +async function serveBatchSettlement( + route: Route, + args: Args, + payload: Record<string, unknown>, + receiverAddress: `0x${string}`, ): Promise<Response> { const network = acceptedNetwork(payload); if (!network || !MAINNET_NETWORKS.includes(network)) { @@ -362,7 +483,10 @@ async function challenge(route: Route, receiverAddress: `0x${string}`): Promise< tags: ["search", "web", "x402"], iconUrl: `${openapiSpec.servers[0].url}/favicon.png`, }); - return toResponse(create402Response(paymentRequirements)); + // Append the exact alternative AFTER the batch entries — order is the contract (see + // withExactAlternative): channel buyers keep their preferred entries, channel-less + // clients fall through to the exact one. + return toResponse(create402Response(withExactAlternative(paymentRequirements, receiverAddress))); } function isHexAddress(addr: unknown): addr is `0x${string}` { diff --git a/bunny_js/src/x402_server.ts b/bunny_js/src/x402_server.ts index 9155796fa..0378e22bf 100644 --- a/bunny_js/src/x402_server.ts +++ b/bunny_js/src/x402_server.ts @@ -491,6 +491,63 @@ export async function createBatchSettlementPaymentRequirements({ }; } +/** + * The exact-scheme alternative the sellers append to their batch-settlement 402s. + * + * Why it exists: a stock x402 client can sign an EIP-3009 authorization but cannot open + * batch-settlement channels — for those callers the channel-only challenge was unpayable. + * Terms are deliberately the flattest possible: one network (Base mainnet), one token + * (USDC), one price per call (10000 atomic = $0.01). + * + * The price IS the facilitator's per-settle fee (0.01), so a settled exact call nets the + * seller zero minus the upstream cost — an accepted trade for the simplicity it buys + * (decided 2026-11). It also means an exact price below $0.01 would lose money on every + * call, which is why /fetch quotes 10000 under exact despite quoting 1000 under + * batch-settlement — the two schemes price independently. + */ +export const EXACT_ALTERNATIVE_NETWORK = "eip155:8453" as const; +export const EXACT_ALTERNATIVE_AMOUNT_ATOMIC = "10000"; + +/** + * The one accepts entry the exact alternative consists of — also the per-call requirements + * the paid path verifies and settles against (single source; see genimg_x402_token.ts for + * the production exact flow this mirrors). + */ +export function createExactAlternative(payTo: string): SdkPaymentRequirements { + const usdc = offeredStablecoins(EXACT_ALTERNATIVE_NETWORK).find((coin) => coin.symbol === "USDC"); + if (!usdc) { + // Config error (chain-utils' token list changed), not a caller error — throws into the + // 402 path's existing try/catch and surfaces as the same "Server configuration error". + throw new Error("USDC is not offered on the exact-alternative network"); + } + return { + scheme: "exact", + network: EXACT_ALTERNATIVE_NETWORK as `${string}:${string}`, + amount: EXACT_ALTERNATIVE_AMOUNT_ATOMIC, + asset: usdc.address, + payTo, + maxTimeoutSeconds: 60, + extra: tokenExtra(usdc), + }; +} + +/** + * Appends the exact alternative AFTER the batch-settlement entries — the order is the + * contract: existing channel buyers read accepts[0..n-1] as the preferred (EURe-first) + * way to pay, and a client that cannot wire batch-settlement falls through to the exact + * entry at the end. The extensions stay the batch builder's own (Permit2/bazaar). + */ +export function withExactAlternative< + T extends { + x402Version: number; + resource: { url: string }; + accepts: unknown[]; + extensions?: Record<string, unknown>; + }, +>(challenge: T, payTo: string): T { + return { ...challenge, accepts: [...challenge.accepts, createExactAlternative(payTo)] }; +} + export interface PaymentRequirementsOptions { resourceUrl: string; description: string; diff --git a/bunny_js/test/llm.test.ts b/bunny_js/test/llm.test.ts index 046f63051..5f8b079b7 100644 --- a/bunny_js/test/llm.test.ts +++ b/bunny_js/test/llm.test.ts @@ -8,6 +8,9 @@ const { mockCreateLLMResourceServer, mockCreateBatchSettlementPaymentRequirements, mockCreate402Response, + mockCreateResourceServer, + mockExactVerifyPayment, + mockExactSettlePayment, mockExtractPaymentPayload, mockCreateSettlementHeaders, mockGetBatchSettlementNetworks, @@ -63,6 +66,10 @@ const { mockCreateLLMResourceServer: vi.fn(), mockCreateBatchSettlementPaymentRequirements: vi.fn(), mockCreate402Response: vi.fn(), + // The exact alternative's server (createResourceServer — no channels, no scheme object). + mockCreateResourceServer: vi.fn(), + mockExactVerifyPayment: vi.fn(), + mockExactSettlePayment: vi.fn(), mockExtractPaymentPayload: vi.fn(), mockCreateSettlementHeaders: vi.fn(), mockGetBatchSettlementNetworks: vi.fn(), @@ -89,28 +96,52 @@ vi.mock("../src/llm_service.js", () => ({ advertisedModelIds: () => ["mistral-large-latest"], })); -vi.mock("../src/x402_server.js", () => ({ - createLLMResourceServer: mockCreateLLMResourceServer, - createBatchSettlementPaymentRequirements: mockCreateBatchSettlementPaymentRequirements, - create402Response: mockCreate402Response, - extractPaymentPayload: mockExtractPaymentPayload, - createSettlementHeaders: mockCreateSettlementHeaders, - getBatchSettlementNetworks: mockGetBatchSettlementNetworks, - releaseLock: mockReleaseLock, - // Real value (the scw-verbatim advertised maxTimeoutSeconds / lock TTL), imported by - // llm_routes.ts — see the constant's twin of this comment there. - LLM_MAX_TIMEOUT_SECONDS: 120, - // Real implementation (pure bigint math, no side effects) — keep in sync with - // x402_server.ts's actual formatUsdcAtomicAsDecimalUsd. - formatUsdcAtomicAsDecimalUsd: (atomicAmount: string) => { - const atomic = BigInt(atomicAmount); - const divisor = 1_000_000n; - const whole = atomic / divisor; - const fraction = (atomic % divisor).toString().padStart(6, "0"); - const trimmedFraction = fraction.replace(/0+$/, ""); - return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); - }, -})); +vi.mock("../src/x402_server.js", async () => { + // The real registry — which asset is offered where is part of what these tests check, so + // the exact-alternative helpers mirror the real implementation against it. + const { USDC_ADDRESSES } = await import("@fretchen/chain-utils"); + const exactAlternativeEntry = (payTo: string) => ({ + scheme: "exact", + network: "eip155:8453", + amount: "10000", + asset: USDC_ADDRESSES["eip155:8453"], + payTo, + maxTimeoutSeconds: 60, + extra: { name: "USD Coin", version: "2" }, + }); + return { + createLLMResourceServer: mockCreateLLMResourceServer, + createBatchSettlementPaymentRequirements: mockCreateBatchSettlementPaymentRequirements, + create402Response: mockCreate402Response, + extractPaymentPayload: mockExtractPaymentPayload, + createSettlementHeaders: mockCreateSettlementHeaders, + getBatchSettlementNetworks: mockGetBatchSettlementNetworks, + releaseLock: mockReleaseLock, + // Real values, as the routes import them. + EXACT_ALTERNATIVE_NETWORK: "eip155:8453", + EXACT_ALTERNATIVE_AMOUNT_ATOMIC: "10000", + // Real semantics for the pure helpers — the tests below pin the entry they produce. + createExactAlternative: exactAlternativeEntry, + withExactAlternative: (challenge: { accepts: unknown[] }, payTo: string) => ({ + ...challenge, + accepts: [...(challenge.accepts ?? []), exactAlternativeEntry(payTo)], + }), + createResourceServer: mockCreateResourceServer, + // Real value (the scw-verbatim advertised maxTimeoutSeconds / lock TTL), imported by + // llm_routes.ts — see the constant's twin of this comment there. + LLM_MAX_TIMEOUT_SECONDS: 120, + // Real implementation (pure bigint math, no side effects) — keep in sync with + // x402_server.ts's actual formatUsdcAtomicAsDecimalUsd. + formatUsdcAtomicAsDecimalUsd: (atomicAmount: string) => { + const atomic = BigInt(atomicAmount); + const divisor = 1_000_000n; + const whole = atomic / divisor; + const fraction = (atomic % divisor).toString().padStart(6, "0"); + const trimmedFraction = fraction.replace(/0+$/, ""); + return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); + }, + }; +}); // The real stablecoin registry — which asset a seller prices is the thing under test here. vi.mock("@fretchen/chain-utils", async (importOriginal) => ({ @@ -240,6 +271,18 @@ describe("sc_llm_x402", () => { transaction: "", network: "eip155:84532", }); + // The exact alternative's server: same shapes, its own mocks, so the two schemes' call + // paths can be asserted apart. + mockCreateResourceServer.mockImplementation(() => ({ + verifyPayment: mockExactVerifyPayment, + settlePayment: mockExactSettlePayment, + })); + mockExactVerifyPayment.mockResolvedValue({ isValid: true, payer: "0xpayer" }); + mockExactSettlePayment.mockResolvedValue({ + success: true, + transaction: "", + network: "eip155:8453", + }); }); describe("basic request validation", () => { @@ -1403,4 +1446,120 @@ describe("sc_llm_x402", () => { expect(mockVerifyPayment).not.toHaveBeenCalled(); }); }); + + describe("the exact alternative", () => { + const exactPayload = { + accepted: { + scheme: "exact", + network: "eip155:8453", + asset: USDC_ADDRESSES["eip155:8453"], + }, + payload: { transferWithAuthorization: "0xsigned" }, + }; + + /** One accepts entry appended AFTER the batch-settlement ones — the ordering is the + * contract (channel buyers read their entries first, channel-less clients fall + * through); the mock's batch accepts are empty, so appended-last also means + * index-0-of-one here. */ + it("appends the exact alternative after the batch-settlement accepts", async () => { + const res = await handle(makeEvent() as never, {}); + + expect(res.statusCode).toBe(402); + const challenge = (mockCreate402Response.mock.calls[0] as unknown[])[0] as { + accepts: Record<string, unknown>[]; + }; + const exactEntries = challenge.accepts.filter((entry) => entry["scheme"] === "exact"); + expect(exactEntries).toHaveLength(1); + expect(challenge.accepts[challenge.accepts.length - 1]).toEqual({ + scheme: "exact", + network: "eip155:8453", + amount: "10000", + asset: USDC_ADDRESSES["eip155:8453"], + payTo: VALID_ADDRESS, + maxTimeoutSeconds: 60, + extra: { name: "USD Coin", version: "2" }, + }); + }); + + it("serves and settles an exact payment at the flat price, metering bypassed", async () => { + mockExtractPaymentPayload.mockReturnValue(exactPayload); + + const res = await handle(makeEvent() as never, {}); + + expect(res.statusCode).toBe(200); + // The exact server, for both payment steps; the batch pipeline never runs. + expect(mockCreateResourceServer).toHaveBeenCalledOnce(); + expect(mockExactVerifyPayment).toHaveBeenCalledTimes(1); + expect(mockExactVerifyPayment).toHaveBeenCalledWith( + exactPayload, + expect.objectContaining({ + scheme: "exact", + network: "eip155:8453", + amount: "10000", + asset: USDC_ADDRESSES["eip155:8453"], + payTo: VALID_ADDRESS, + }), + ); + // Flat price: the settle amount is exactly what was verified, NOT the usage-derived + // getSettleAmount of the batch path. + expect(mockExactSettlePayment).toHaveBeenCalledTimes(1); + expect((mockExactSettlePayment.mock.calls[0] as unknown[])[1]).toEqual( + expect.objectContaining({ scheme: "exact", amount: "10000" }), + ); + expect(res.headers?.["Payment-Response"] ?? res.headers?.["payment-response"]).toBe( + "encoded", + ); + expect(mockCreateLLMResourceServer).not.toHaveBeenCalled(); + expect(mockEnhancePaymentRequirements).not.toHaveBeenCalled(); + expect(mockVerifyPayment).not.toHaveBeenCalled(); + expect(mockSettlePayment).not.toHaveBeenCalled(); + // Mistral really ran (mainnet offer — no mock completion unless asked). + expect(mockCallLLMAPI).toHaveBeenCalledTimes(1); + expect(mockCallLLMAPI.mock.calls[0][1]).toBe(false); // useMock + }); + + it("rejects an exact payment naming a network it never offered", async () => { + mockExtractPaymentPayload.mockReturnValue({ + accepted: { + scheme: "exact", + network: "eip155:84532", // Base Sepolia — batch-settlement testnet, never exact + asset: USDC_ADDRESSES["eip155:84532"], + }, + payload: { transferWithAuthorization: "0xsigned" }, + }); + + const res = await handle(makeEvent() as never, {}); + + expect(res.statusCode).toBe(402); + expect(mockExactVerifyPayment).not.toHaveBeenCalled(); + expect(mockExactSettlePayment).not.toHaveBeenCalled(); + expect(mockCallLLMAPI).not.toHaveBeenCalled(); + }); + + it("answers an invalid exact payment with a plain 402, not the batch corrective", async () => { + mockExtractPaymentPayload.mockReturnValue(exactPayload); + mockExactVerifyPayment.mockResolvedValue({ isValid: false, invalidReason: "expired" }); + + const res = await handle(makeEvent() as never, {}); + + expect(res.statusCode).toBe(402); + expect(JSON.parse(res.body).error).toMatch(/expired/); + // No channel state to resync — no SDK corrective re-challenge. + expect(mockCreatePaymentRequiredResponse).not.toHaveBeenCalled(); + expect(mockExactSettlePayment).not.toHaveBeenCalled(); + expect(mockCallLLMAPI).not.toHaveBeenCalled(); + }); + + it("does not settle an exact payment when the model call fails", async () => { + mockExtractPaymentPayload.mockReturnValue(exactPayload); + mockCallLLMAPI.mockRejectedValue(new Error("mistral down")); + + const res = await handle(makeEvent() as never, {}); + + expect(res.statusCode).toBe(500); + expect(mockExactSettlePayment).not.toHaveBeenCalled(); + // Exact holds no lock, so the batch path's lock release must not run either. + expect(mockReleaseLock).not.toHaveBeenCalled(); + }); + }); }); diff --git a/bunny_js/test/routes.test.ts b/bunny_js/test/routes.test.ts index bb8972b9c..46d02fbe0 100644 --- a/bunny_js/test/routes.test.ts +++ b/bunny_js/test/routes.test.ts @@ -29,6 +29,9 @@ const { mockCreatePaymentRequiredResponse, mockEnhancePaymentRequirements, mockReleaseLock, + mockCreateResourceServer, + mockExactVerifyPayment, + mockExactSettlePayment, } = vi.hoisted(() => ({ mockCreateLLMResourceServer: vi.fn(), mockCreateBatchSettlementPaymentRequirements: vi.fn(), @@ -40,16 +43,54 @@ const { mockCreatePaymentRequiredResponse: vi.fn(), mockEnhancePaymentRequirements: vi.fn(), mockReleaseLock: vi.fn(), + // The exact alternative's server (createResourceServer — no channels, no scheme object). + mockCreateResourceServer: vi.fn(), + mockExactVerifyPayment: vi.fn(), + mockExactSettlePayment: vi.fn(), })); -vi.mock("../src/x402_server.js", () => ({ - createLLMResourceServer: mockCreateLLMResourceServer, - createBatchSettlementPaymentRequirements: mockCreateBatchSettlementPaymentRequirements, - create402Response: mockCreate402Response, - extractPaymentPayload: mockExtractPaymentPayload, - createSettlementHeaders: mockCreateSettlementHeaders, - releaseLock: mockReleaseLock, -})); +vi.mock("../src/x402_server.js", async () => { + // The real registry — which asset is offered where is part of what these tests check, so + // the exact-alternative helpers mirror the real implementation against it. + const { USDC_ADDRESSES } = await import("@fretchen/chain-utils"); + const exactAlternativeEntry = (payTo: string) => ({ + scheme: "exact", + network: "eip155:8453", + amount: "10000", + asset: USDC_ADDRESSES["eip155:8453"], + payTo, + maxTimeoutSeconds: 60, + extra: { name: "USD Coin", version: "2" }, + }); + return { + createLLMResourceServer: mockCreateLLMResourceServer, + createBatchSettlementPaymentRequirements: mockCreateBatchSettlementPaymentRequirements, + create402Response: mockCreate402Response, + extractPaymentPayload: mockExtractPaymentPayload, + createSettlementHeaders: mockCreateSettlementHeaders, + releaseLock: mockReleaseLock, + // Real values, as the routes import them. + EXACT_ALTERNATIVE_NETWORK: "eip155:8453", + EXACT_ALTERNATIVE_AMOUNT_ATOMIC: "10000", + // Real implementation (pure bigint math, no side effects) — keep in sync with + // x402_server.ts's actual formatUsdcAtomicAsDecimalUsd. + formatUsdcAtomicAsDecimalUsd: (atomicAmount: string) => { + const atomic = BigInt(atomicAmount); + const divisor = 1_000_000n; + const whole = atomic / divisor; + const fraction = (atomic % divisor).toString().padStart(6, "0"); + const trimmedFraction = fraction.replace(/0+$/, ""); + return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); + }, + // Real semantics for the pure helpers — the tests below pin the entry they produce. + createExactAlternative: exactAlternativeEntry, + withExactAlternative: (challenge: { accepts: unknown[] }, payTo: string) => ({ + ...challenge, + accepts: [...(challenge.accepts ?? []), exactAlternativeEntry(payTo)], + }), + createResourceServer: mockCreateResourceServer, + }; +}); /** The seller's own address — `NFT_WALLET_PUBLIC_KEY`, shared with the chat so both land on one * payment channel. Anvil account #2, standing in for it. */ @@ -180,6 +221,18 @@ beforeEach(async () => { mockExtractPaymentPayload.mockReturnValue(null); mockVerifyPayment.mockResolvedValue({ isValid: true, payer: "0xpayer" }); mockSettlePayment.mockResolvedValue({ success: true, transaction: "", network: "eip155:10" }); + // The exact alternative's server: same shapes, its own mocks, so the two schemes' call + // paths can be asserted apart. + mockCreateResourceServer.mockImplementation(() => ({ + verifyPayment: mockExactVerifyPayment, + settlePayment: mockExactSettlePayment, + })); + mockExactVerifyPayment.mockResolvedValue({ isValid: true, payer: "0xpayer" }); + mockExactSettlePayment.mockResolvedValue({ + success: true, + transaction: "", + network: "eip155:8453", + }); mockCreatePaymentRequiredResponse.mockResolvedValue({ x402Version: 2, error: "channel_busy", @@ -567,9 +620,15 @@ describe("the 402 challenge", () => { ])("quotes %s at %s atomic units", async (route, amount) => { const res = await handle(makeRequest("GET", route, { auth: null })); - const { accepts } = JSON.parse(res.body) as { accepts: { amount: string }[] }; + const { accepts } = JSON.parse(res.body) as { + accepts: { amount: string; scheme?: string }[]; + }; expect(res.statusCode).toBe(402); - expect(accepts.every((entry) => entry.amount === amount)).toBe(true); + // The route's own price applies to every entry except the exact alternative (whose + // flat per-call price is pinned in its own test below). + const routeEntries = accepts.filter((entry) => entry.scheme !== "exact"); + expect(routeEntries.length).toBeGreaterThan(0); + expect(routeEntries.every((entry) => entry.amount === amount)).toBe(true); }); /** @@ -588,12 +647,21 @@ describe("the 402 challenge", () => { }); /** Testnet USDC is free and both routes spend real money, so a testnet payment would buy - * metered Brave queries for nothing. */ + * metered Brave queries for nothing. The exact alternative is Base MAINNET — it joins the + * mainnet-only fold rather than widening it. */ test("offers mainnet only", async () => { const res = await handle(makeRequest("GET", "search", { auth: null })); - const { accepts } = JSON.parse(res.body) as { accepts: { network: string }[] }; - expect(accepts.map((entry) => entry.network).sort()).toEqual(["eip155:10", "eip155:8453"]); + const { accepts } = JSON.parse(res.body) as { accepts: { network: string; scheme?: string }[] }; + expect( + accepts + .filter((entry) => entry.scheme !== "exact") + .map((entry) => entry.network) + .sort(), + ).toEqual(["eip155:10", "eip155:8453"]); + expect( + accepts.every((entry) => entry.network === "eip155:10" || entry.network === "eip155:8453"), + ).toBe(true); }); test("quotes the route, not the query string, as the resource", async () => { @@ -859,3 +927,124 @@ describe("the paid path", () => { expect(mockSettlePayment).toHaveBeenCalled(); }); }); + +/** + * The exact alternative: the accepts entry appended AFTER the batch-settlement entries — + * one EIP-3009 signature per call, flat $0.01 USDC on Base mainnet, no channels. It exists + * for stock x402 clients that cannot open channels at all. + */ +describe("the exact alternative", () => { + const exactPayload = { + accepted: { + scheme: "exact", + network: "eip155:8453", + asset: USDC_ADDRESSES["eip155:8453"], + }, + payload: { transferWithAuthorization: "0xsigned" }, + }; + + /** One accepts entry, appended last, exact terms; the batch entries stay untouched ahead + * of it — the ordering IS the contract (channel buyers read their entries first). */ + test("appends one exact entry after the batch-settlement entries", async () => { + for (const route of ["search", "fetch"] as const) { + const res = await handle(makeRequest("GET", route, { auth: null })); + + const { accepts } = JSON.parse(res.body) as { + accepts: Record<string, unknown>[]; + }; + const exactEntries = accepts.filter((entry) => entry["scheme"] === "exact"); + expect(exactEntries).toHaveLength(1); + expect(accepts[accepts.length - 1]["scheme"]).toBe("exact"); + expect(exactEntries[0]).toEqual({ + scheme: "exact", + network: "eip155:8453", + amount: "10000", + asset: USDC_ADDRESSES["eip155:8453"], + payTo: RECEIVER, + maxTimeoutSeconds: 60, + extra: { name: "USD Coin", version: "2" }, + }); + } + }); + + test("serves and settles an exact payment without touching channel machinery", async () => { + mockExtractPaymentPayload.mockReturnValue(exactPayload); + + const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); + + expect(res.statusCode).toBe(200); + // The exact server, for both payment steps; the batch server is never built. + expect(mockCreateResourceServer).toHaveBeenCalledOnce(); + expect(mockExactVerifyPayment).toHaveBeenCalledTimes(1); + expect(mockExactVerifyPayment).toHaveBeenCalledWith( + exactPayload, + expect.objectContaining({ + scheme: "exact", + network: "eip155:8453", + amount: "10000", + asset: USDC_ADDRESSES["eip155:8453"], + payTo: RECEIVER, + maxTimeoutSeconds: 60, + }), + ); + // Flat price: settle verifies and settles the same amount (no metering, no enhancement). + expect(mockExactSettlePayment).toHaveBeenCalledTimes(1); + expect((mockExactSettlePayment.mock.calls[0] as unknown[])[1]).toEqual( + expect.objectContaining({ scheme: "exact", amount: "10000" }), + ); + expect(res.headers["payment-response"]).toBe("encoded"); + // None of the channel pipeline may run. + expect(mockCreateLLMResourceServer).not.toHaveBeenCalled(); + expect(mockEnhancePaymentRequirements).not.toHaveBeenCalled(); + expect(mockVerifyPayment).not.toHaveBeenCalled(); + expect(mockSettlePayment).not.toHaveBeenCalled(); + expect(fetch).toHaveBeenCalledTimes(1); // the paid Brave call did run + }); + + test("rejects an exact payment on network terms it never offered", async () => { + mockExtractPaymentPayload.mockReturnValue({ + accepted: { + scheme: "exact", + network: "eip155:10", // Optimism USDC — offered for batch-settlement, never for exact + asset: USDC_ADDRESSES["eip155:10"], + }, + payload: { transferWithAuthorization: "0xsigned" }, + }); + + const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); + + expect(res.statusCode).toBe(402); + expect(mockExactVerifyPayment).not.toHaveBeenCalled(); + expect(mockExactSettlePayment).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("answers an invalid exact payment with a plain 402, not the batch corrective", async () => { + mockExtractPaymentPayload.mockReturnValue(exactPayload); + mockExactVerifyPayment.mockResolvedValue({ isValid: false, invalidReason: "expired" }); + + const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); + + expect(res.statusCode).toBe(402); + expect(JSON.parse(res.body).error).toMatch(/expired/); + // No channel state to resync — so no SDK corrective and no advert of the failed payment. + expect(mockCreatePaymentRequiredResponse).not.toHaveBeenCalled(); + expect(mockExactSettlePayment).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); + }); + + test("does not settle an exact payment when the upstream fails", async () => { + mockExtractPaymentPayload.mockReturnValue(exactPayload); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ ok: false, status: 502, statusText: "Bad Gateway" }), + ); + + const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); + + expect(res.statusCode).toBe(500); + expect(mockExactSettlePayment).not.toHaveBeenCalled(); + // Exact holds no lock, so the batch path's lock release must not run either. + expect(mockReleaseLock).not.toHaveBeenCalled(); + }); +}); From 640afae9402f36c04131fc331ce6f116a38ed252 Mon Sep 17 00:00:00 2001 From: fretchen <polka.99gefiedert@icloud.com> Date: Wed, 11 Nov 2026 22:22:26 +0100 Subject: [PATCH 6/8] root pages: per-token prices, EURe where accepted, fetch on the gateway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three agent complaints, one KISS rule: a page always states the schemes it accepts and prices EVERY accepted token, generated from the same PriceList constants the challenge quotes (formatParallelPrices in both x402_server twins — EURe, EURC, USDC in the seller's preference order, EUR leading where the euro tokens are accepted, decimals from the token registry). Nothing token-ish appears on one page and not another. - web-agent + llm-agent price lines now carry €/$ per token per route (plus the flat exact alternative), and one standardized Payment paragraph on both: both schemes — channels first, exact appended — networks/tokens, retry-same-request. - openapi.search.json's description said "USDC (or EURC on Base)" — stale from before EURe; now names all three tokens and both schemes (it is the page's summary). openapi.llm.json's description gets the same skeleton. - genimg's page showed only the $0.07 figure although its EUR price is €0.06 — now ßper-token via the same twin helper (. price and page share PRICE_ATOMIC). - The MCP gateway page never mentioned /fetch — the web seller's most-called route is REST-only (not an MCP tool): its seller link, a price bullet, and the payment paragraph now name it, say where it lives, and note the two schemes so an MCP-only agent can find it. Tests: root-page pins updated to the generated per-token strings (EURe-first, exact flat price), the gateway page pins GET /fetch; both suites and the twins' x402_server stay in sync. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> --- bunny_js/src/llm_routes.ts | 5 +++-- bunny_js/src/mcp_routes.ts | 16 ++++++++++------ bunny_js/src/openapi.llm.json | 2 +- bunny_js/src/openapi.search.json | 2 +- bunny_js/src/searchapi_routes.ts | 16 ++++++---------- bunny_js/src/x402_server.ts | 16 ++++++++++++++++ bunny_js/test/llm.test.ts | 13 +++++++++++++ bunny_js/test/mcp.test.ts | 4 ++++ bunny_js/test/routes.test.ts | 25 ++++++++++++++++++++++--- scw_js/genimg_x402_token.ts | 16 +++++----------- scw_js/test/genimg_x402_token.test.ts | 6 +++++- scw_js/x402_server.ts | 16 ++++++++++++++++ 12 files changed, 102 insertions(+), 35 deletions(-) diff --git a/bunny_js/src/llm_routes.ts b/bunny_js/src/llm_routes.ts index 0a26454bd..72392d285 100644 --- a/bunny_js/src/llm_routes.ts +++ b/bunny_js/src/llm_routes.ts @@ -50,6 +50,7 @@ import { createResourceServer, extractPaymentPayload, createSettlementHeaders, + formatParallelPrices, getBatchSettlementNetworks, LLM_MAX_TIMEOUT_SECONDS, formatUsdcAtomicAsDecimalUsd, @@ -108,9 +109,9 @@ const ROOT_PAGE = buildRootPage({ }, ], price: [ - `POST / — metered by real token usage, at most $${formatUsdcAtomicAsDecimalUsd(MAX_PRICE_ATOMIC.USDC)} per request`, + `POST / — metered per request, ceiling ${formatParallelPrices(MAX_PRICE_ATOMIC)}; or exact: $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} flat (USDC, Base mainnet)`, ], - payment: `x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — two schemes are listed: batch-settlement channels first (metered by real token usage under a per-request ceiling), and an exact alternative at the end for clients without channel wiring — one EIP-3009 signature per call, flat $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} in USDC on Base mainnet. Retry the same request with the payment attached.`, + payment: `x402 (HTTP 402): no account, no API key. The 402 lists both schemes — batch-settlement channels first (the preferred way to pay: metered by real token usage under the ceiling) and an exact alternative at the end (one EIP-3009 signature per call, unmetered, for clients without channel wiring). Pay in USDC (Base or Optimism) or EURC/EURe (Base); the challenge carries every token's per-network terms. Retry the same request with the payment attached.`, tryIt: `curl -X POST '${openapiSpec.servers[0].url}/' -H 'Content-Type: application/json' -d '{"model":"${advertisedModelIds()[0]}","messages":[{"role":"user","content":"ping"}]}'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.\n# Testnet note: on Base Sepolia the payment settles and the completion is a mocked answer — a free end-to-end test.`, errors: "402 = unpaid or invalid payment (challenge in the Payment-Required header); 400 = invalid request (OpenAI-shaped error body); 404 = unknown model. Response schemas: openapi.json.", diff --git a/bunny_js/src/mcp_routes.ts b/bunny_js/src/mcp_routes.ts index 29d187938..75eb1439d 100644 --- a/bunny_js/src/mcp_routes.ts +++ b/bunny_js/src/mcp_routes.ts @@ -54,19 +54,23 @@ const ROOT_PAGE = buildRootPage({ }, { href: "https://web-agent.fretchen.eu/", - label: "web_search — the web seller's page, spec and terms", + label: + "the web seller — home of the web_search tool; it also serves GET /fetch (single-page fetch, one URL's clean machine-readable content), a REST route priced on its page", }, { href: `https://mcp-agent.fretchen.eu/${AGENT_REGISTRATION_PATH}`, label: "this origin's registration document (domain control proof for both agents)", }, ], - price: MCP_TOOLS.map( - (tool) => - `${tool.name} — priced at the seller it relays to; the unpaid call's tool result carries the exact 402 terms`, - ), + price: [ + ...MCP_TOOLS.map( + (tool) => + `${tool.name} — priced at the seller it relays to; the unpaid call's tool result carries the exact 402 terms`, + ), + "GET /fetch — single-page fetch, the web seller's most-called route: REST only (no MCP tool — call the web seller directly), priced on its page", + ], payment: - "per tool, via x402 at the relaying seller: no account, no API key. initialize and tools/list are free; an unpaid tools/call returns the 402 demands as structuredContent in an isError tool result — retry with the payment attached.", + "per tool, via x402 at the relaying seller: no account, no API key. Each seller's 402 lists both schemes — batch-settlement channels first (USDC, or EURC/EURe on Base) and an exact alternative (one signature per call, flat in USDC on Base mainnet); the challenge carries every token's terms. initialize and tools/list are free; an unpaid tools/call returns the 402 demands as structuredContent in an isError tool result — retry with the payment attached.", tryIt: `curl -X POST '${MCP_SERVICE.endpoint}' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'\n# → the tools and their input schemas, free (as is initialize). tools/call carries the x402 payment.`, errors: "JSON-RPC errors: -32601 unknown method (a stateless server: initialize, tools/list, tools/call only), -32602 unknown tool. GET /mcp is 405 Allow: POST (the optional GET stream is not offered). Unpaid tools/call is NOT an error — it is a tools result with isError and the 402 demands in structuredContent.", diff --git a/bunny_js/src/openapi.llm.json b/bunny_js/src/openapi.llm.json index 5d2b27000..ab7dde8a5 100644 --- a/bunny_js/src/openapi.llm.json +++ b/bunny_js/src/openapi.llm.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Fretchen LLM Inference — pay-per-call chat completions for agents", - "description": "OpenAI-shaped chat completions from Mistral models, sold per request to autonomous agents via x402 batch-settlement payment channels — no account, no API key. Metered by real token usage under a fixed per-request ceiling; settled in USDC (Base or Optimism), EURC or EURe (Base), while Base Sepolia settles for free and answers with a mocked completion.", + "description": "OpenAI-shaped chat completions from Mistral models, sold per request to autonomous agents via x402 — batch-settlement channels (metered by real token usage under a per-request ceiling; USDC, EURC or EURe, the euro tokens on Base) or a flat per-call exact alternative (unmetered, USDC on Base mainnet). No account, no API key; Base Sepolia settles for free and answers with a mocked completion.", "version": "1.0.0", "x-guidance": "OpenAI chat-completions body. POST / with { model, messages: [{ role, content }, ...] } and no payment header to receive a 402 with x402 batch-settlement payment requirements (accepts[]). Open/top up a payment channel per the requirements, retry with the payment header, and the service returns a standard OpenAI chat.completion object. Streaming (stream: true) is not supported. Each message is metered and settled up to a per-message price ceiling; the real cost is usage-derived and typically lower. Other standard OpenAI chat params (temperature, top_p, stop, seed, the penalties, response_format) are forwarded to the upstream model and work as normal; they are not enumerated here because the upstream owns that contract. The exceptions are stream, n and max_tokens, which are rejected with a 400 rather than ignored, because each would move cost past the fixed per-message ceiling this endpoint meters against. Tool calling is supported (see x-capabilities): send OpenAI-shaped tools, and a tool call comes back as choices[].message.tool_calls with finish_reason: \"tool_calls\" and content: null. Execute it yourself and send the result back as a role:\"tool\" message with the matching tool_call_id — this endpoint never calls a tool on your behalf, and accepts no callback or MCP server URL. Note: payment uses x402 batch-settlement (a stateful channel), so a stock OpenAI SDK cannot pay this endpoint without batch-settlement client wiring — the OpenAI shape is for body legibility, not drop-in SDK use. After the channel entries, the 402 also carries one exact-scheme alternative: a single EIP-3009 signature per call, flat 0.01 USDC on Base mainnet, unmetered — the usage-derived settlement and the per-message ceiling apply to channel payments only — for clients that cannot open channels at all. The live challenge is the source of truth for its terms.", "contact": { diff --git a/bunny_js/src/openapi.search.json b/bunny_js/src/openapi.search.json index 6fd5443a0..1772cc0c8 100644 --- a/bunny_js/src/openapi.search.json +++ b/bunny_js/src/openapi.search.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Fretchen Web Retrieval — pay-per-call web access for agents", - "description": "Web search and single-page fetch for agents, paid per call via x402 batch-settlement payment channels in USDC (or EURC on Base).", + "description": "Web search and single-page fetch for agents, paid per call via x402 — batch-settlement channels in USDC, EURC or EURe (the euro tokens on Base, USDC also on Optimism), or a flat per-call exact alternative in USDC on Base mainnet.", "version": "1.0.0", "x-guidance": "GET /search?q=... or GET /fetch?url=... with no payment header to receive a 402 carrying x402 batch-settlement payment requirements (accepts[]). Open or top up a payment channel per those requirements, retry with the payment header, and the service returns JSON. Payment travels in the header only — these are GET routes, so there is no body fallback. Both routes bill onto the SAME channel as llm-agent.fretchen.eu when the payer, voucher signer and receiver match, so an agent already chatting there pays for web access without a second deposit. Prices are fixed per route and settled only when the call succeeds: an upstream failure returns 500 and takes no money. Mainnet only — testnet USDC is free and these routes spend real money on the caller's behalf. After the channel entries, the 402 also carries one exact-scheme alternative: a single EIP-3009 signature per call, flat 0.01 USDC on Base mainnet, priced independently of the channel prices — for clients that cannot open batch-settlement channels. The live challenge is the source of truth for its terms.", "contact": { diff --git a/bunny_js/src/searchapi_routes.ts b/bunny_js/src/searchapi_routes.ts index df45bdf00..9bdc0d79b 100644 --- a/bunny_js/src/searchapi_routes.ts +++ b/bunny_js/src/searchapi_routes.ts @@ -46,6 +46,7 @@ import { createResourceServer, createSettlementHeaders, extractPaymentPayload, + formatParallelPrices, formatUsdcAtomicAsDecimalUsd, releaseLock, withExactAlternative, @@ -108,16 +109,11 @@ const ROOT_PAGE = buildRootPage({ label: "stateless MCP endpoint — same tools as JSON-RPC 2.0 over POST (web_search)", }, ], - price: ROUTES.map((route) => { - const info = ( - openapiSpec.paths as Record< - string, - Record<string, { "x-payment-info"?: { price: { amount: string } } }> - > - )[`/${route}`].get["x-payment-info"]; - return `GET /${route} — $${info?.price.amount ?? "?"} per call`; - }), - payment: `x402 (HTTP 402): no account, no API key. An unpaid request answers 402 with the machine-readable Payment-Required challenge — two schemes are listed: batch-settlement channels first (the preferred way to pay), and an exact alternative at the end for clients without channel wiring — one EIP-3009 signature per call, flat $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} in USDC on Base mainnet. Retry the same request with the payment attached.`, + price: ROUTES.map( + (route) => + `GET /${route} — channel call: ${formatParallelPrices(PRICE_ATOMIC[route])}; or exact: $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} flat (USDC, Base mainnet)`, + ), + payment: `x402 (HTTP 402): no account, no API key. The 402 lists both schemes — batch-settlement channels first (the preferred way to pay) and an exact alternative at the end (one EIP-3009 signature per call, for clients without channel wiring). Pay in USDC (Base or Optimism) or EURC/EURe (Base); the challenge carries every token's per-network terms. Retry the same request with the payment attached.`, tryIt: `curl '${SERVICE_URL}/search?q=erc-8004'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.`, errors: "402 = unpaid (challenge in the Payment-Required header); 404 = no such route; everything else is standard HTTP. Response schemas: openapi.json.", diff --git a/bunny_js/src/x402_server.ts b/bunny_js/src/x402_server.ts index 0378e22bf..368521859 100644 --- a/bunny_js/src/x402_server.ts +++ b/bunny_js/src/x402_server.ts @@ -744,6 +744,22 @@ export function formatUsdcAtomicAsDecimalUsd(atomicAmount: string, decimals: big return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); } +/** + * A PriceList as the root pages display it: every accepted token priced in its own units, in + * the seller's preference order (EURe, EURC, USDC on Base — the same order the 402 lists), + * € for the euro tokens and $ for USDC, decimals read from the token registry so EURe's + * 18-decimal prices format correctly. Pages built with this can never quote fewer tokens + * (or a different order) than the challenge does. + */ +export function formatParallelPrices(price: PriceList): string { + return offeredStablecoins("eip155:8453") + .map((coin) => { + const display = formatUsdcAtomicAsDecimalUsd(price[coin.symbol], BigInt(coin.decimals)); + return `${coin.symbol === "USDC" ? "$" : "€"}${display} in ${coin.symbol}`; + }) + .join(", "); +} + export function createSettlementHeaders( settlementResult: Record<string, unknown>, ): Record<string, string> { diff --git a/bunny_js/test/llm.test.ts b/bunny_js/test/llm.test.ts index 5f8b079b7..4cbbd0673 100644 --- a/bunny_js/test/llm.test.ts +++ b/bunny_js/test/llm.test.ts @@ -140,6 +140,19 @@ vi.mock("../src/x402_server.js", async () => { const trimmedFraction = fraction.replace(/0+$/, ""); return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); }, + // Real semantics, EURe-first as the production preference orders it: every token priced + // in its own units, so the page can never quote fewer tokens than the challenge. + formatParallelPrices: (price: { EURe: string; EURC: string; USDC: string }) => { + const fmt = (atomic: string, decimals: bigint, symbol: "EURe" | "EURC" | "USDC") => + `${symbol === "USDC" ? "$" : "€"}${(() => { + const a = BigInt(atomic); + const div = 10n ** decimals; + const whole = a / div; + const frac = (a % div).toString().padStart(Number(decimals), "0").replace(/0+$/, ""); + return frac ? `${whole}.${frac}` : whole.toString(); + })()} in ${symbol}`; + return `${fmt(price.EURe, 18n, "EURe")}, ${fmt(price.EURC, 6n, "EURC")}, ${fmt(price.USDC, 6n, "USDC")}`; + }, }; }); diff --git a/bunny_js/test/mcp.test.ts b/bunny_js/test/mcp.test.ts index 2fc276c9d..e02112f22 100644 --- a/bunny_js/test/mcp.test.ts +++ b/bunny_js/test/mcp.test.ts @@ -231,6 +231,10 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { // the free first look: initialize and tools/list respond unpaid. expect(body).toContain("https://llm-agent.fretchen.eu/"); expect(body).toContain("https://web-agent.fretchen.eu/"); + // /fetch is REST-only (no MCP tool) — the page must still name the seller's + // most-called route and where it lives, or an MCP-only agent never finds it. + expect(body).toContain("GET /fetch"); + expect(body).toContain("single-page fetch"); expect(body).toContain(".well-known/agent-registration.json"); expect(body).toContain("https://mcp-agent.fretchen.eu/mcp"); expect(body).toContain("tools/list"); diff --git a/bunny_js/test/routes.test.ts b/bunny_js/test/routes.test.ts index 46d02fbe0..81c7e9813 100644 --- a/bunny_js/test/routes.test.ts +++ b/bunny_js/test/routes.test.ts @@ -82,6 +82,19 @@ vi.mock("../src/x402_server.js", async () => { const trimmedFraction = fraction.replace(/0+$/, ""); return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); }, + // Real semantics, EURe-first as the production preference orders it: every token priced + // in its own units, so the page can never quote fewer tokens than the challenge. + formatParallelPrices: (price: { EURe: string; EURC: string; USDC: string }) => { + const fmt = (atomic: string, decimals: bigint, symbol: "EURe" | "EURC" | "USDC") => + `${symbol === "USDC" ? "$" : "€"}${(() => { + const a = BigInt(atomic); + const div = 10n ** decimals; + const whole = a / div; + const frac = (a % div).toString().padStart(Number(decimals), "0").replace(/0+$/, ""); + return frac ? `${whole}.${frac}` : whole.toString(); + })()} in ${symbol}`; + return `${fmt(price.EURe, 18n, "EURe")}, ${fmt(price.EURC, 6n, "EURC")}, ${fmt(price.USDC, 6n, "USDC")}`; + }, // Real semantics for the pure helpers — the tests below pin the entry they produce. createExactAlternative: exactAlternativeEntry, withExactAlternative: (challenge: { accepts: unknown[] }, payTo: string) => ({ @@ -594,9 +607,15 @@ describe("discovery", () => { expect(res.body).toContain(".well-known/agent-registration.json"); expect(res.body).toContain(".well-known/oasf.json"); expect(res.body).toContain("https://mcp-agent.fretchen.eu/mcp"); - // Prices restated on the page must be the spec's x-payment-info values. - expect(res.body).toContain("GET /search — $0.01 per call"); - expect(res.body).toContain("GET /fetch — $0.001 per call"); + // Prices on the page are generated from PRICE_ATOMIC — every accepted token in its + // own units, EURe first as the seller's preference orders it — plus the flat exact + // alternative; never a single-token restatement. + expect(res.body).toContain( + "GET /search — channel call: €0.01 in EURe, €0.01 in EURC, $0.01 in USDC; or exact: $0.01 flat (USDC, Base mainnet)", + ); + expect(res.body).toContain( + "GET /fetch — channel call: €0.001 in EURe, €0.001 in EURC, $0.001 in USDC; or exact: $0.01 flat (USDC, Base mainnet)", + ); expect(res.body).toContain("curl 'https://web-agent.fretchen.eu/search?q=erc-8004'"); expect(res.body).toContain("402"); expect(mockCreateBatchSettlementPaymentRequirements).not.toHaveBeenCalled(); diff --git a/scw_js/genimg_x402_token.ts b/scw_js/genimg_x402_token.ts index fcdca287f..d131e94a2 100644 --- a/scw_js/genimg_x402_token.ts +++ b/scw_js/genimg_x402_token.ts @@ -33,8 +33,9 @@ import { createResourceServer, createPaymentRequirements, create402Response, - extractPaymentPayload, createSettlementHeaders, + extractPaymentPayload, + formatParallelPrices, type SdkPaymentPayload, type SdkPaymentRequirements, } from "./x402_server.js"; @@ -88,16 +89,9 @@ const ROOT_PAGE = buildRootPage({ }, ], price: [ - ((): string => { - // Generated from the committed spec, not restated — the page and the 402 share it. - const info = ( - openapiSpec.paths as Record< - string, - Record<string, { "x-payment-info"?: { price: { amount: string } } }> - > - )["/"].post["x-payment-info"]; - return `POST /imagegen — $${info?.price.amount ?? "?"} per image (USDC, EURC or EURe; each token's price is set in its own units)`; - })(), + // Generated from the committed price list — every accepted token in its own units, + // EURe first as the seller's preference orders it; never a single-token restatement. + `POST /imagegen — ${formatParallelPrices(PRICE_ATOMIC)} per image`, ], payment: "x402 (HTTP 402), exact scheme — one signature moves the money per call: no account, no API key, no approve() needed. An unpaid request answers 402 with the machine-readable Payment-Required challenge — exact terms (network, token, amount) are in the challenge, then retry the same request with the payment attached.", diff --git a/scw_js/test/genimg_x402_token.test.ts b/scw_js/test/genimg_x402_token.test.ts index 0b7905651..fe7524793 100644 --- a/scw_js/test/genimg_x402_token.test.ts +++ b/scw_js/test/genimg_x402_token.test.ts @@ -666,7 +666,11 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { expect(response.body).toContain(".well-known/agent-registration.json"); expect(response.body).toContain(".well-known/oasf.json"); // The page price is the spec's x-payment-info value, not a hand-restated one. - expect(response.body).toContain("POST /imagegen — $0.07 per image"); + // Generated per-token prices (EURe first, each token's own units) — nominal EUR + // and USD prices differ here, which is exactly why the page must quote both. + expect(response.body).toContain( + "POST /imagegen — €0.06 in EURe, €0.06 in EURC, $0.07 in USDC per image", + ); expect(response.body).toContain( "curl -X POST 'https://imagegen-agent.fretchen.eu/imagegen'", ); diff --git a/scw_js/x402_server.ts b/scw_js/x402_server.ts index 9155796fa..d8df04d10 100644 --- a/scw_js/x402_server.ts +++ b/scw_js/x402_server.ts @@ -687,6 +687,22 @@ export function formatUsdcAtomicAsDecimalUsd(atomicAmount: string, decimals: big return trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString(); } +/** + * A PriceList as the root pages display it: every accepted token priced in its own units, in + * the seller's preference order (EURe, EURC, USDC on Base — the same order the 402 lists), + * € for the euro tokens and $ for USDC, decimals read from the token registry so EURe's + * 18-decimal prices format correctly. Pages built with this can never quote fewer tokens + * (or a different order) than the challenge does. (Twin of bunny_js/x402_server.ts.) + */ +export function formatParallelPrices(price: PriceList): string { + return offeredStablecoins("eip155:8453") + .map((coin) => { + const display = formatUsdcAtomicAsDecimalUsd(price[coin.symbol], BigInt(coin.decimals)); + return `${coin.symbol === "USDC" ? "$" : "€"}${display} in ${coin.symbol}`; + }) + .join(", "); +} + export function createSettlementHeaders( settlementResult: Record<string, unknown>, ): Record<string, string> { From 447a8953128eb27a080965ee8f9164dae844eb9e Mon Sep 17 00:00:00 2001 From: fretchen <polka.99gefiedert@icloud.com> Date: Thu, 12 Nov 2026 06:49:18 +0100 Subject: [PATCH 7/8] web-agent: marked testnet stand-in answers on Base Sepolia MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A paid request on Base Sepolia now runs the full x402 machinery (402 challenge, payment validation, channel verify, settle-on-success, the Payment-Response header) but is answered by deterministic, loudly marked stand-ins (src/search_stand_in.ts) instead of the metered upstreams — matching the llm-agent's testnet behavior. Testnet money (free tokens) never buys Brave queries or /fetch egress; mainnet payments always get the real thing, and the exact scheme remains Base mainnet only. - searchapi_routes: OFFERED_NETWORKS from getBatchSettlementNetworks() (adds the testnet mock path), run() takes useMock=isTestnet(network), root page carries a Base Sepolia test note. - search_stand_in: standInSearch()/standInFetch() returning the services' own answer shapes, echoing the input and carrying the marker. - openapi.search: guidance flipped — Base Sepolia settles free test tokens for stand-in answers; metered upstreams run for mainnet only. - tests: routes + mcp pin the invariant in both directions (testnet settles without touching fetch; mainnet still calls Brave). Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> --- bunny_js/src/openapi.search.json | 2 +- bunny_js/src/search_stand_in.ts | 66 +++++++++++++++++++++++++ bunny_js/src/searchapi_routes.ts | 43 ++++++++++++----- bunny_js/test/mcp.test.ts | 32 +++++++----- bunny_js/test/routes.test.ts | 83 ++++++++++++++++++++++++++++++-- 5 files changed, 195 insertions(+), 31 deletions(-) create mode 100644 bunny_js/src/search_stand_in.ts diff --git a/bunny_js/src/openapi.search.json b/bunny_js/src/openapi.search.json index 1772cc0c8..d148e061d 100644 --- a/bunny_js/src/openapi.search.json +++ b/bunny_js/src/openapi.search.json @@ -4,7 +4,7 @@ "title": "Fretchen Web Retrieval — pay-per-call web access for agents", "description": "Web search and single-page fetch for agents, paid per call via x402 — batch-settlement channels in USDC, EURC or EURe (the euro tokens on Base, USDC also on Optimism), or a flat per-call exact alternative in USDC on Base mainnet.", "version": "1.0.0", - "x-guidance": "GET /search?q=... or GET /fetch?url=... with no payment header to receive a 402 carrying x402 batch-settlement payment requirements (accepts[]). Open or top up a payment channel per those requirements, retry with the payment header, and the service returns JSON. Payment travels in the header only — these are GET routes, so there is no body fallback. Both routes bill onto the SAME channel as llm-agent.fretchen.eu when the payer, voucher signer and receiver match, so an agent already chatting there pays for web access without a second deposit. Prices are fixed per route and settled only when the call succeeds: an upstream failure returns 500 and takes no money. Mainnet only — testnet USDC is free and these routes spend real money on the caller's behalf. After the channel entries, the 402 also carries one exact-scheme alternative: a single EIP-3009 signature per call, flat 0.01 USDC on Base mainnet, priced independently of the channel prices — for clients that cannot open batch-settlement channels. The live challenge is the source of truth for its terms.", + "x-guidance": "GET /search?q=... or GET /fetch?url=... with no payment header to receive a 402 carrying x402 batch-settlement payment requirements (accepts[]). Open or top up a payment channel per those requirements, retry with the payment header, and the service returns JSON. Payment travels in the header only — these are GET routes, so there is no body fallback. Both routes bill onto the SAME channel as llm-agent.fretchen.eu when the payer, voucher signer and receiver match, so an agent already chatting there pays for web access without a second deposit. Prices are fixed per route and settled only when the call succeeds: an upstream failure returns 500 and takes no money. Base Sepolia is offered for end-to-end testing: a testnet payment settles in free test tokens and the answer is a marked stand-in (no web search or fetch is performed) — the metered upstreams run for mainnet payments only. After the channel entries, the 402 also carries one exact-scheme alternative: a single EIP-3009 signature per call, flat 0.01 USDC on Base mainnet, priced independently of the channel prices — for clients that cannot open batch-settlement channels. The live challenge is the source of truth for its terms.", "contact": { "name": "fretchen", "url": "https://www.fretchen.eu", diff --git a/bunny_js/src/search_stand_in.ts b/bunny_js/src/search_stand_in.ts new file mode 100644 index 000000000..cfb6f5322 --- /dev/null +++ b/bunny_js/src/search_stand_in.ts @@ -0,0 +1,66 @@ +/** + * The testnet stand-ins for the web seller's two routes — the twin's counterpart of + * llm_service's mock LLM (scw parity in intent, not in shape). + * + * A paid request on a testnet network (Base Sepolia) exercises the FULL x402 machinery — + * challenge, validation, channel verification, settlement, the Payment-Response header — + * but must never touch the metered upstreams: Brave bills per query and /fetch is + * egress, and a testnet payment is free. `run()` in searchapi_routes.ts therefore swaps + * these in for searchWeb/fetchExternalHtml whenever the payment's network is a testnet. + * + * The output types are the SERVICES' own return types (SearchResult / FetchedPage), so the + * rest of the response path is untouched. Content rules, deliberately strict: + * - deterministic — same input, same bytes; no clock, no randomness, no drift; + * - loud — every string carries the MOCKER marker, so no caller can mistake a stand-in + * for real data (a mock that looks real is worse than no mock); + * - echoing — the input query/url appears in the output, so the caller sees their own + * request flow through the whole pipeline. + */ + +import type { SearchResult } from "./search_service.js"; +import type { FetchedPage } from "./web_fetch_service.js"; + +/** The marker every stand-in string carries — tests pin the exact phrase. */ +export const TESTNET_MOCK_MARKER = "testnet mock"; + +const note = + "This is a testnet mock answer: your payment's challenge, verification, settlement and " + + "response all ran, but no real web search was performed. Pay on a mainnet network " + + "(eip155:10 or eip155:8453) for real results."; + +export function standInSearch(query: string): { results: SearchResult[] } { + return { + results: [ + { + url: "https://web-agent.fretchen.eu/mock-result-1", + title: `testnet mock result for "${query}"`, + text: note, + }, + { + url: "https://web-agent.fretchen.eu/mock-result-2", + title: "testnet mock: payment pipeline verified", + text: `Your query "${query}" reached the seller and a signed, settled payment was accepted for it.`, + }, + { + url: "https://web-agent.fretchen.eu/mock-result-3", + title: "testnet mock: how to get real results", + text: "Aim the same client at a mainnet network entry of the 402 challenge and the live upstream runs.", + }, + ], + }; +} + +export function standInFetch(url: string): FetchedPage { + return { + finalUrl: url, + contentType: "text/html; charset=utf-8", + html: `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><title>testnet mock page: ${escapeHtml( + url, + )}\n\n

testnet mock page

\n

${note}

\n

You requested ${escapeHtml( + url, + )}; it was never fetched.

\n\n`, + }; +} + +const escapeHtml = (text: string): string => + text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); diff --git a/bunny_js/src/searchapi_routes.ts b/bunny_js/src/searchapi_routes.ts index 9bdc0d79b..f3921aebd 100644 --- a/bunny_js/src/searchapi_routes.ts +++ b/bunny_js/src/searchapi_routes.ts @@ -12,9 +12,11 @@ * fails after it never settles, orphaning the lock for MAX_TIMEOUT_SECONDS). */ import { logger } from "./logger.js"; +import { isTestnet } from "@fretchen/chain-utils"; import { offeredStablecoins, resolvePaidStablecoin } from "./stablecoin_pricing.js"; import { searchWeb, QueryError } from "./search_service.js"; import { fetchExternalHtml, FetchUrlError, parseHttpsUrl } from "./web_fetch_service.js"; +import { standInFetch, standInSearch } from "./search_stand_in.js"; import { DESCRIPTION, FetchQuerySchema, @@ -48,6 +50,7 @@ import { extractPaymentPayload, formatParallelPrices, formatUsdcAtomicAsDecimalUsd, + getBatchSettlementNetworks, releaseLock, withExactAlternative, type SdkPaymentPayload, @@ -55,13 +58,14 @@ import { } from "./x402_server.js"; /** - * Mainnet only, unlike the chat, which also accepts Base Sepolia. Testnet USDC is free, and - * both routes spend real money on the caller's behalf — Brave bills per query, and /fetch is - * egress. Advertising a testnet would hand anyone a metered API for nothing. The cost is - * that the paid path cannot be exercised on a testnet at all: at $0.001 a call, a real - * run is cheaper than the setup it replaces. (Copied from scw_js/search_api.ts.) + * The batch-settlement networks, mainnet PLUS Base Sepolia — unlike the scw original, which + * was mainnet-only. Adverting a testnet is safe now BECAUSE the paid path never touches the + * metered upstreams there: a testnet payment gets the marked stand-in answers instead + * (search_stand_in.ts), while the full challenge/verify/settle machinery runs for real on + * free test tokens — the try-before-buy the agents asked for. Metered Brave/egress only + * run for mainnet payments; the regression test pins that forever. */ -const MAINNET_NETWORKS = ["eip155:10", "eip155:8453"]; +const OFFERED_NETWORKS = getBatchSettlementNetworks(); /** * Advertised and verified lock TTL, a quarter of the chat's LLM_MAX_TIMEOUT_SECONDS. It @@ -114,7 +118,7 @@ const ROOT_PAGE = buildRootPage({ `GET /${route} — channel call: ${formatParallelPrices(PRICE_ATOMIC[route])}; or exact: $${formatUsdcAtomicAsDecimalUsd(EXACT_ALTERNATIVE_AMOUNT_ATOMIC)} flat (USDC, Base mainnet)`, ), payment: `x402 (HTTP 402): no account, no API key. The 402 lists both schemes — batch-settlement channels first (the preferred way to pay) and an exact alternative at the end (one EIP-3009 signature per call, for clients without channel wiring). Pay in USDC (Base or Optimism) or EURC/EURe (Base); the challenge carries every token's per-network terms. Retry the same request with the payment attached.`, - tryIt: `curl '${SERVICE_URL}/search?q=erc-8004'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.`, + tryIt: `curl '${SERVICE_URL}/search?q=erc-8004'\n# → HTTP 402 + Payment-Required header: the terms. No wallet needed for this first look.\n# Testnet note: a payment on Base Sepolia settles in free test tokens and the answer is a marked mock — the full flow, real results on mainnet only.`, errors: "402 = unpaid (challenge in the Payment-Required header); 404 = no such route; everything else is standard HTTP. Response schemas: openapi.json.", }); @@ -166,7 +170,16 @@ function parseArgs(route: Route, queryParams: Record): Args { return { route, url: parseHttpsUrl(parsed.data.url).toString() }; } -function run(args: Args): Promise { +/** + * The upstream call for one paid request — the ONE place the metered services are touched, + * which is why the testnet gate lives here and nowhere else: a testnet payment gets the + * marked stand-in answers (search_stand_in.ts) and the metered upstreams never run. + */ +async function run(args: Args, useMock: boolean): Promise { + if (useMock) { + logger.info({ route: args.route }, "Serving stand-in answer (testnet payment)"); + return args.route === "search" ? standInSearch(args.q) : standInFetch(args.url); + } return args.route === "search" ? searchWeb(args.q) : fetchExternalHtml(args.url); } @@ -279,7 +292,8 @@ async function serveExact( let data: unknown; try { - data = await run(args); + // Exact is offered on Base mainnet only — the metered upstream always runs there. + data = await run(args, false); } catch (err) { // Nothing settled and nothing to release — exact holds no locks. logger.warn({ err, route }, "Run failed after exact verify"); @@ -322,11 +336,14 @@ async function serveBatchSettlement( receiverAddress: `0x${string}`, ): Promise { const network = acceptedNetwork(payload); - if (!network || !MAINNET_NETWORKS.includes(network)) { + if (!network || !OFFERED_NETWORKS.includes(network)) { return jsonResponse(402, { - error: `Unsupported or missing network. This endpoint accepts ${MAINNET_NETWORKS.join(", ")}`, + error: `Unsupported or missing network. This endpoint accepts ${OFFERED_NETWORKS.join(", ")}`, }); } + // A testnet payment settles free test tokens and answers with the marked stand-in + // (see run()); the metered upstreams run for mainnet payments only. + const useMock = isTestnet(network); // Price the stablecoin the buyer's channel is in (see sc_llm_x402.ts) — checked before any // server setup, since an unoffered asset is the caller's error, not ours. @@ -412,7 +429,7 @@ async function serveBatchSettlement( let data: unknown; try { - data = await run(args); + data = await run(args, useMock); } catch (err) { // Nothing is settled: an upstream that failed is not something to charge for. But verify took // the channel lock, which the chat shares — release it, or the user's next message is @@ -470,7 +487,7 @@ async function challenge(route: Route, receiverAddress: `0x${string}`): Promise< price: PRICE_ATOMIC[route], payTo: receiverAddress, scheme, - networks: MAINNET_NETWORKS, + networks: OFFERED_NETWORKS, maxTimeoutSeconds: MAX_TIMEOUT_SECONDS, // One bazaar block per route: the block names the method and the query parameters, and the // two routes sell different things at different prices. (Copied.) diff --git a/bunny_js/test/mcp.test.ts b/bunny_js/test/mcp.test.ts index e02112f22..133cb827b 100644 --- a/bunny_js/test/mcp.test.ts +++ b/bunny_js/test/mcp.test.ts @@ -343,7 +343,7 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { // ════════════════════════════════════════════════════════════════════════════ describe("tools/call — the payment gate (no free path, by the real handlers)", () => { - it("answers an unpaid web_search with the real mainnet-only demands and never touches Brave", async () => { + it("answers an unpaid web_search with the real demands and never touches Brave", async () => { const res = await rpc({ jsonrpc: "2.0", id: 3, @@ -359,11 +359,11 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { expect(result.isError).toBe(true); expect(result.structuredContent.x402Version).toBe(2); expect(result.structuredContent.accepts.length).toBeGreaterThan(0); - // The security pin, on the real challenge path: search is mainnet-only — testnet - // coins must never buy metered Brave queries. A translation layer that minted its - // own accepts broke this once; it cannot anymore. + // Mainnets plus Base Sepolia — testnet is offered because a testnet payment buys the + // MARKED STAND-IN, never metered Brave. A translation layer that minted its own + // accepts broke the network pin once; it cannot anymore. for (const accept of result.structuredContent.accepts) { - expect(["eip155:10", "eip155:8453"]).toContain(accept.network); + expect(["eip155:10", "eip155:8453", "eip155:84532"]).toContain(accept.network); } expect(mockSearchWeb).not.toHaveBeenCalled(); }); @@ -429,7 +429,7 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { expect(result._meta["x402/payment-response"]).toMatchObject({ success: true }); }); - it("rejects a paid web_search on testnet with the handler's own network refusal", async () => { + it("runs and settles a paid web_search on testnet via the handler's own stand-in rule", async () => { const res = await rpc( { jsonrpc: "2.0", @@ -441,15 +441,21 @@ describe("mcp_routes (stateless paid MCP endpoint)", () => { ); if ("error" in res) throw new Error(`expected result, got: ${JSON.stringify(res).slice(0, 300)}`); + expect(mockVerifyPayment).toHaveBeenCalledTimes(1); + expect(mockSettlePayment).toHaveBeenCalledTimes(1); + // The REAL testnet-stand-in rule, exercised through MCP: a worthless-coin payment + // must buy the marked mock answer, never metered Brave. + expect(mockSearchWeb).not.toHaveBeenCalled(); const result = res.result as { - isError: boolean; - structuredContent: { error: string }; + isError?: boolean; + structuredContent: { results: Array<{ title: string; text: string }> }; + _meta: Record; }; - expect(result.isError).toBe(true); - expect(result.structuredContent.error).toContain("Unsupported or missing network"); - // Refused before anything metered runs. - expect(mockSearchWeb).not.toHaveBeenCalled(); - expect(mockVerifyPayment).not.toHaveBeenCalled(); + expect(result.isError).not.toBe(true); + expect(result.structuredContent.results[0].title).toContain("testnet mock"); + expect(result.structuredContent.results[0].text).toContain("testnet mock"); + // The settlement receipt, decoded from the handler's Payment-Response header. + expect(result._meta["x402/payment-response"]).toMatchObject({ success: true }); }); it("runs and settles a paid web_search on mainnet and returns the receipt", async () => { diff --git a/bunny_js/test/routes.test.ts b/bunny_js/test/routes.test.ts index 81c7e9813..81657d648 100644 --- a/bunny_js/test/routes.test.ts +++ b/bunny_js/test/routes.test.ts @@ -7,6 +7,7 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; // being asserted is that even a CORRECT signature is ignored. import { privateKeyToAccount } from "viem/accounts"; import { USDC_ADDRESSES, EURC_ADDRESSES } from "@fretchen/chain-utils"; +import { TESTNET_MOCK_MARKER } from "../src/search_stand_in.js"; // DNS is stubbed so these stay hermetic: /fetch resolves every hostname before connecting, and a // test that needs a working resolver fails on an offline machine for reasons that have nothing to @@ -72,6 +73,8 @@ vi.mock("../src/x402_server.js", async () => { // Real values, as the routes import them. EXACT_ALTERNATIVE_NETWORK: "eip155:8453", EXACT_ALTERNATIVE_AMOUNT_ATOMIC: "10000", + // Real value (x402_server.ts's batch networks — mainnets plus the testnet mock path). + getBatchSettlementNetworks: () => ["eip155:10", "eip155:8453", "eip155:84532"], // Real implementation (pure bigint math, no side effects) — keep in sync with // x402_server.ts's actual formatUsdcAtomicAsDecimalUsd. formatUsdcAtomicAsDecimalUsd: (atomicAmount: string) => { @@ -668,7 +671,7 @@ describe("the 402 challenge", () => { /** Testnet USDC is free and both routes spend real money, so a testnet payment would buy * metered Brave queries for nothing. The exact alternative is Base MAINNET — it joins the * mainnet-only fold rather than widening it. */ - test("offers mainnet only", async () => { + test("offers the batch networks: mainnets plus the testnet mock path", async () => { const res = await handle(makeRequest("GET", "search", { auth: null })); const { accepts } = JSON.parse(res.body) as { accepts: { network: string; scheme?: string }[] }; @@ -677,9 +680,14 @@ describe("the 402 challenge", () => { .filter((entry) => entry.scheme !== "exact") .map((entry) => entry.network) .sort(), - ).toEqual(["eip155:10", "eip155:8453"]); + ).toEqual(["eip155:10", "eip155:8453", "eip155:84532"]); expect( - accepts.every((entry) => entry.network === "eip155:10" || entry.network === "eip155:8453"), + accepts.every( + (entry) => + entry.network === "eip155:10" || + entry.network === "eip155:8453" || + entry.network === "eip155:84532", + ), ).toBe(true); }); @@ -903,7 +911,9 @@ describe("the paid path", () => { }); test("refuses a payment on a network it does not sell on", async () => { - mockExtractPaymentPayload.mockReturnValue(payment("eip155:84532")); + // Optimism Sepolia: in no network list here — not batch (no contract deployment there) + // and not the exact alternative (Base mainnet only). The refusal stays a plain 402. + mockExtractPaymentPayload.mockReturnValue(payment("eip155:11155420")); const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); @@ -1067,3 +1077,68 @@ describe("the exact alternative", () => { expect(mockReleaseLock).not.toHaveBeenCalled(); }); }); + +/** + * The testnet stand-in: a paid request on Base Sepolia runs the FULL x402 machinery — + * challenge, validation, channel verify, settle-on-success, the Payment-Response header — + * but answers with the marked stand-in (search_stand_in.ts) instead of the metered + * upstreams. The invariant is one-sided and absolute: testnet money (free) must never + * buy Brave queries or /fetch egress; mainnet money must always get the real thing. + */ +describe("the testnet stand-in", () => { + beforeEach(() => { + mockExtractPaymentPayload.mockReturnValue(payment("eip155:84532")); + }); + + test("serves and settles a paid /search with the marked stand-in, upstream untouched", async () => { + const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); + + expect(res.statusCode).toBe(200); + expect(mockVerifyPayment).toHaveBeenCalledTimes(1); + expect(mockSettlePayment).toHaveBeenCalledTimes(1); + expect(res.headers["payment-response"]).toBe("encoded"); + // The security invariant: testnet money never reaches Brave. + expect(fetch).not.toHaveBeenCalled(); + + const body = JSON.parse(res.body) as { + results: { title: string; text: string; url: string }[]; + }; + expect(body.results).toHaveLength(3); + for (const result of body.results) { + expect(result.title).toContain(TESTNET_MOCK_MARKER); + expect(result.url).toContain("web-agent.fretchen.eu"); + } + // Echo: the caller's own query arrives in the stand-in answer. + expect(body.results[0].title).toContain("x402"); + }); + + test("serves and settles a paid /fetch with the marked stand-in, upstream untouched", async () => { + const res = await handle( + makeRequest("GET", "fetch", { auth: null, query: { url: "https://example.com/" } }), + ); + + expect(res.statusCode).toBe(200); + expect(mockVerifyPayment).toHaveBeenCalledTimes(1); + expect(mockSettlePayment).toHaveBeenCalledTimes(1); + expect(res.headers["payment-response"]).toBe("encoded"); + // The security invariant: testnet money never triggers egress. + expect(fetch).not.toHaveBeenCalled(); + + const body = JSON.parse(res.body) as { finalUrl: string; html: string; contentType: string }; + expect(body.finalUrl).toBe("https://example.com/"); + expect(body.html).toContain(TESTNET_MOCK_MARKER); + expect(body.html).toContain("no real web"); // loud it is + }); + + /** The other side of the invariant, pinned where it is defined: the same caller, the same + * route, only the network flipped — and the metered upstream runs. */ + test("flips back to the real upstream for a mainnet payment", async () => { + mockExtractPaymentPayload.mockReturnValue(payment("eip155:10")); + + const res = await handle(makeRequest("GET", "search", { auth: null, query: { q: "x402" } })); + + expect(res.statusCode).toBe(200); + expect(fetch).toHaveBeenCalledTimes(1); // the paid Brave call did run + expect(JSON.stringify(res.body)).not.toContain(TESTNET_MOCK_MARKER); + }); +}); From d987f588eaefde26ca3180f559a55fca34b6eb43 Mon Sep 17 00:00:00 2001 From: fretchen Date: Thu, 12 Nov 2026 07:09:27 +0100 Subject: [PATCH 8/8] agents: serve the current fretchen logo as the favicon The /favicon.png served on every ERC-8004 origin (and linked first in each root page, where x402scan reads it) was a stale JPEG. Regenerate favicon.ts in both seller packages from the live website logo (website/public/fretchen.png, 512x512 PNG); the /favicon.png path and the base64-embed mechanism stay as they are. - bunny_js + scw_js: favicon.ts re-encoded, Content-Type image/png. - discovery.ts twins: type updated. - tests: content-type pins flipped, magic bytes pin the PNG signature (0x8950) instead of the JPEG SOI marker. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- bunny_js/src/discovery.ts | 2 +- bunny_js/src/favicon.ts | 4 ++-- bunny_js/test/llm.test.ts | 8 ++++---- bunny_js/test/routes.test.ts | 2 +- scw_js/discovery.ts | 2 +- scw_js/favicon.ts | 4 ++-- scw_js/test/genimg_x402_token.test.ts | 10 +++++----- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bunny_js/src/discovery.ts b/bunny_js/src/discovery.ts index 4b488eb95..9197c4eae 100644 --- a/bunny_js/src/discovery.ts +++ b/bunny_js/src/discovery.ts @@ -40,7 +40,7 @@ export function buildRootPage(input: RootPageInput): string { - + ${escapeHtml(input.title)} diff --git a/bunny_js/src/favicon.ts b/bunny_js/src/favicon.ts index 6693e3ff5..922b31605 100644 --- a/bunny_js/src/favicon.ts +++ b/bunny_js/src/favicon.ts @@ -1,3 +1,3 @@ export const faviconBase64 = - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAQABAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDLopTSV9EeSJS0UUxBSUtFABRRRQAUUUUAJRS4ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACkpaKAEopaKACilpKACiiigAoopcUAJRRRQAUUUUAFJS0UAFFFFACUUtFABRRRQAlFLRQAUUUUAFJS0UAFFFFABRRRQAlLRRQAUUUUAFFFFABRRRQAlFLRQAUUUUAFFFFABRRRQAlFLRQAUUUUAJS0lLQAUUUUAFFFFABRRRQAUUUUAFJS0UAJRS0UAFFFFACUtFFACUUtFACUtFFABRRRQAUUUUAJRS0UAJS0UUAFFFFABRRRQAUUUUAFJS0UAJRS0UAJRS0UAFFFKKQAaSlNFACUUUUwCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKWgBKKWkoAKKKKACiiigAooooAWkoooAKKKWgBKKKKAFpKWg0AJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUUtACUUGigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWiigAooopABpKKKYBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFGaKKAFpKKKACiiigAooooAKKKWgApKWigBKKKKACilooAKQ0tJQAUUUUAFFFFABRRRQAUUUUAFFFLQAlFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFLQAlLRRQAlFFFABRS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUooAKSlNFIBKKKKYBRRRQAUUUUAFFFFABRRRQAUUUtABSUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUtJQAUUUtABRRRQAUUUUAFJS0lABRRS0AJS0UUAJRRRigAopaMUAJRS0UAJRRRQAUUUUAFFFFAAKWiigBKKKWgBKKKKACiiigAoopcUAFJS0lABRRS0AJRS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAAKWikoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaSloAKKKKQCUUUUwCiiigAooooAKKKKAClpKWgBKKKKACiiigAooooAKKKKACiiloASilpKACilooASilpDQAUUUUAFBopaAEopaKAEopaKAEopaSgBaSlooAKSlpKACiiigApaSloAKKSloAM0UlLQAUlLSUAFLSUtABSUtJQAUUUUAFFFFABRRRQMKKKKAFooooEFJS0lAC0UUUAFJS0lABRRRQAUtJRQAUUUUALSUtIaACiiigApaKKACkpaKAEooooAWikooAKKKKACiiigAooooAKKKKAClpKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWkooAKKKKACiiigAooooAKKKWgBKWilFACUUUUgEooopgFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJRQAtJS0lABRRRQAZooooAKKKKACiiigApaSigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACilooASloooAKSlpKACiiigAooooAKKWigBKKWkoAWikpaACkpaSgAooooAKKKKACiiigAooooAKWkpaACkpaKAEooooGFLSUUCCiiigAooooAKKWigBKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooASloooAKKKKACiiigBaKSloAKKKKQCUUUUwCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKBhRRRQIKKWkoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAopaKAEooooAWkoooAWkpaMUAJRS0UAJRRRQAtFJS0AFFFFACUUUUAFFLRQAlFFFABRRRQAUUUUAFFFFABS0lFAC0lLRQAlFFFABRRRQAUUUYoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKWkNABRRRQAUUUUAFFFFABRRRQAUtJSigBKWiikAlFFFMAooooAKKKKACiiigAooooAKWkpaACiikoAWkpaSgAopcUlABS0lLQAlFFFABRRRQAUUUUAFFGKKACiiigAooooAKKKKACilooASloooASilooASiiigAoopaACiiigAopKKACilpKACiiigYtFJS0hBSGlooASiilpgJRRRQAUUUUAFFFFAC0UUlAC0lLSUAFFFFABRRRQAUUUUAFFFFAC0lFFABRRRQAUUUtACUtJRQAtJS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALSUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lKKACiiikAlFLSUwCiiigAooooAKKKKACiiigAooooAKKKKACiiigBaKKKAEopaSgAooooAKKKKACiiigBaSiigAooooAKKKKACiiloAKKKKACiiigAooooASiiigAopaKAEoopaAEopaSgBaSiigAooooGFLSUtAgpKKKACjNFFABRRRQAUUUUAFFFFAC0GiigBKKKKACiiigAooooAKKWjFACUUUUAFFFFABRRRQMKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRS0AFFFFIAopKKYBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAC0lFLQAlFLSUALmkoooAKKKKACiiigAooooAKWkooAWkpaKAEopaSgAooooAWiiigAooooAKKKKAEooooAWiiigBKWkooAWkpaSgAooooAKKWigBKKWkNABRRRQAUUUYoAKKWigBKWkpaACkoooAWkoooAKKKKACiiigAooooAWikooAWkpaSgAopaSgAooooAKKWkoAKKKKACiiigAoopaACkpaSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClpKUUAFFFFIBKKKKYBRRRQAopKWkoAKKKKACiiigAooooAKKKKAClpKKACiiigAooooAKKKKACiiigAooooAKKKKAFopKXNABSUtJQAUUUUAFFFFABRRRQAUUUUAFFLSUAFFFFABRRRQAUUUUAFLRRQAUUUlAC0lLRQAlLRRQAUUUGgBKKKWgBKKKKACiiigYUUuKSgQUUUUAFFFFABRRRQAUUUUAFFBooAWkpaQ0AFFFFABRRRQAUUUUAFFFFABRRRQAUUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAwpwpKKBBRRRSASiiimAUUUUAFFFFABRRRQAUUUUAFLSUtABSUUUAFFLRQAlFFFABRRRQAUUUUAFFFFABRRS0AFFFFACUUtJQAtJS0lABRRRQAUUUUALRSUtACUUUUAFFFFABRS0lABRRS0AJRRRQAtFFFAAaSlpKAFooooASilpKACiiloASilpKACilpKBhRRRQIWkoooAKWkooAKKKKAFopKWgAoopDQAUUUUALRSUtACUUtJQAUUUUDCiiigQUtFFABSUtJQAtJRRQAUUUUAFFLSUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AFFFFIBKKWkpgFFFFABRRRQAUUtJQAUUUUAFFFFABRRS0AFFFJQAUUUUAFLSUUAFFFFABRS0UAJRS0hoAWikpaAEooooAUUUUUAFJS0UAJRSmkoAKKKKACiiigYUUUtAgooooAKKKKACiiigAooooAKSlooAKSlooASilpKACiiloASiiigBaSiigAopaKAEooooAKKKKAFpKWkoAKKKKAFpKKKAFoNFFACUtJRQAtJS0UAJRS0mKACilooASiiigBaSiigAooooAKKKKAFpDRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AFFFFIBKKKWmAlFFFABRRRQAUUUtACUUUUAFFLSUAFLSUUALSUtJQAUUUUAFFFFABRRRQAUUUUAGaKKKAClpKKACiiigAopaKACiikoAKKKWgAooooAKKKKACiikNAC0UlLQAUUlFAC0lLRQAUUUUAFFFJQAtFFFABSUUUALRSZpc0AJRRRQAUtAooAKKKKACkpaQ0ALSUUUAFFFFABRRRQAUUUtABRRSUAFFFFABRRRQAtFJS0AIaKKKACiiloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKACiiikAUlFFMAooooAKKKKACilooASiiigAooooAWiiigAoNFFACUUUUAFFFFABQaKWgBKKKKACiiigAooooAKWkooAKKKKAFpKKKACiiigBaSlooAKQ0tJQAUUUUALSUUtACUUUUALRRRQAUlLRQAUlFFAC0lLRQAlFFFABRRRQAUUUUAFFFFAC0UUUAJRRRQAUUUUAFFFFABRRRQAUtJS0AFJS0UAJRS0lABRRRQAUtFIaACiiigAooooAKKKKACiiigApaSloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAFpKKKACiiigAooooAKKKKACiiigAooooAKWkooGLRRRQIKKKKQCUUUUwCiiigAooooAKKKKACiiigAoopaACikpaACg0UlABRRSigBKKWigBKKWkoAKKKKACiiigAooooAKKKKACiiigAooooAKKKWgAooooAKKKKAEopaSgAooooAKKKKACiiigBaKKKAEopaSgBRRSUUAFFLSUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFLRQAlFFFABRRRQMWkpaKBBSUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALSUUUAFFFFABRS0UAJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AKaSlNJSAKDRRTASiiigAooooAKKBRQAUUUUAFLRRQAUlLSUAFFFFABS0lFAC0UlFAC0lLQaAEooooAKKKKACiiigAooooAKKKKACiiloASlopKAClpKKAFopKKAFpKKWgBBS0UUAJRRRQAUUUUALRRRQAUlLSUAFFFFABRRRQAUUlFADiCMZBGelJV/UohFbaaQPv2oY/99tVCkndXGFFFFMQUUUUAFFFFABRRRQAtJRRQAUUUUALSUUtACUuaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKWigBKWkpaAA0UUUgCiiimAlFFLQAlFFFAC0lFFABRRRQAUUUUAFFFFABRRS0AFJS0lABRRRQAtFJS0AJRRS0AJRRRQAtJRRQAUUUUAFFFFABS0lLQAUlLRQAlFFFABRRS0AJS0UUAFFFJQAUUUUAFFFFAC0lFFABRRRQAUUUUAFFFSRxGVtoZAf9tgo/M0AR0YrSh0SefBF1YID/fu0H9a2NO8ET3s6q2q6YoPa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iigAooooAKKKKAFpKKKACiiigAooooAWkoooAKKKKACiiigBaSiloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAopaSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWkpaACkpaKQCUUUUwCiiigAooooAKKKKACilpKACiiigAooooAKKWigBKWkpaACg0UlABRRS0AJRS0lABRRRQAUUtFABSUtFACUUUtABSUUUDCiiigQUUUUALQaSigApaSjNABRRRQAUUUUAFFFFABRRRQAUUUUAFKBSYq3p9jNqV7HawDLuevYDuTSbsrsaVzT8NaEdYvN0gItYiDIf7x7KK9PjRY41RFCqowAOgFVtO0+HTLKO1gGEQcnux7k1YmlSCF5ZGCogLMx6AV5dWo6kjrhHlRT1bVIdJsXuZiMjhEzyzdhXlF9ezahdyXM7bpHOT7ew9qu+INak1rUDJysCfLEnoPU+5rJrtoUeRXe5z1J8zCiiiugzCiiigAooooAKKKKAOq8Ia/8AYbgWNy3+jyn5WJ+43+B/z3r0QHNeIivQ/CGv/bIBY3T5njHyMTy6/wCI/wA964cTRt7yOilPozT8Q6JHrNkVAC3CZMT+/ofY15ZLG8MjRyIUdSQynqD6V7XXI+MdBFxC2pWy/vYx+9UfxL6/UfyqcPWs+Vjqwvqjz+kzSmkr0DmFooooAKKKKACiiigAooooAKKKKACiiigAoopaAEopaSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKAEooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAFoopRQAlJS0lIYUUUUxBRRRQAUUtFACUUUUAFFFFABRRiloAKSiigBaKSloAKSlooASiiigAooooAWiiigBKKKKAFoopKAFoNFFACUtFFACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUoFADkVnYKoJYnAA716d4a0JdIs98oBupRmQ/3f9kVj+DvD+Nup3Sf9cFI/8e/wrtOlefia13yo6KUOrFzXn/jHxAbqZtOtX/cRn96wP329PoP51t+Lde/s6zNrbvi6mGMjqi9z9fSvN8VWGo/bYVZ/ZQlFLRXcc4lFLilEbnorH8KLgNop/kyf883/AO+TS+RL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U7pWBEUQPLn/AA9TVzUtRt9LsZbu6cJHGMn1J7AepNeJa5rdxrupSXc/yqfljjzwi9hV4ah7SV3sTWqcqstytqOoXGqX0t3dOXlkOT6D0A9hVSiivZSSVkee3cKKKKYgooooAWikpc0AFJS5pKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDT0a68m58pj8knH0PaujPIrilJByDyK62yuBc2iSdyOfr3rCpHW5rB9DndStvs14wAwjfMtU66PWrYS2nmAfNGc/h3rnK0hK6IkrMKWkpRVkigV2fgXTd002oyLwn7uP69z/AE/GuPijaWRI0Us7EKoHcmvXtMsU07T4bVMfu1wT6nufzrlxM7Rsa0o3dy52rgfHep+bcxadG3yxfPJ/vHoPy/nXb3t3HY2c1zKcJEpY/wCFeO3dw93dS3Ehy8jFj+NYYWF5cz6GtaVlYhooor0TlCiiigAooooAKKKKACiiigAooooAKKKKACiiigAoopaAEooooAKKKKACiilNACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFLSUooAKKKKQCUUUUwCiiigAooooAKKKKAFopKWgApKWkoAKKWigBKKKKAFpKWigAooooAKSlFFABRSUtABSUtJQAUUUUALRSUtACUUUUAFFFFABRRRQAUUUooAmtbmS0uoriI4eNgy/UV7BZXUd9ZQ3UX3JUDD29q8aruvAup74JdOkPzR/vI8+h6j8/51yYqF48yNqMrOx1GpWKajp09pJjEi4B9D2P515BNC8ErxSKVdGKsD2Ir2mvPPG+m/ZtSW8RcR3A+b2cdfzH9aywtSz5WXWj1OUpRRSgV6BzGxoNvukknI+78q/XvW27BEZieAMmq+nW/wBnsIkIwxG4/U1DrM3lWDqD80hCj+tcz96RstInO3MxuLh5W6sajpKBXRsYnsXw+cP4ThA/hkcH/vquoJABJOAOprzb4e+IbSxhn069nWEM/mRO5wvTBGe3SrXjfxjAbN9M0yZZGlGJpozkKvdQfU/y/TyKlGUqzSR3wqJU0znPGfiQ65qXkwOfsUBIjx0c92/w9vrXMUlFepTgoR5UcUpOTuwooorQkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAWkNFFABRRRQAtbOgz/NJAe43D+v9Kxans5zbXUcvYHn6VM1dDi7M62RQ6FSMgjBrj54jBM8Z/hYiuyHIB7Guf12DZcJMBw4wfqP8isabs7Gs1pcyaUUlOUFiABkmtzE6nwTpf2nUWvZBmO3+77uen5dfyr0TGKzNA03+zNGggYYkI3yf7x6/wCFaE8yW8Ek0hwkalmPsK8qtNzmdkFyxON8eaptSLTYzycSS49Ow/r+VcLVrUr2TUdRnu5PvSNnHoOw/KqtejShyRSOacrsKKKK1ICiiigAooooAKKKWgBKKKKACiiigAooooAKKKKACiiigAooooAKKWkoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoopRQMKKKKQhKKKKYBRRRQAUUUUAFFFGKACilooAKKSloAKKKSgAooooAWiiigBKKKKAClpKWgApKKKACiiigAooooAKKKKACiiloGJRS0UCEooooAKWkooAXNXNMvn07UYLpM5jbJHqO4/KqVKKTV1Yadnc9rhlSeFJY23I6hlPqDWdr+mDVNImgAzIBvjP+0On+H41k+CNT+06c1lIf3lufl91P+B/pXV15Mk6czsTUoniDAqxBGCOxq1p0H2m9jQj5c5b6Ctbxhpn2DWGlRcRXGXGOx7j+v403QLfbHJcEfe+Vf616XOnG6OTl96xs9q53XJvMuViHRB+proZGCIWJwAMmuOnlM07yH+Jiaikru5c3pYhopTSV0GQZooooAKKKKACiiigAooooAKKKWgBKKKKACiiigAooooAKKKKACiiigAooooAKKKKACkoooAWiiigAooooA6vS5/PsIyeoG0/hSarb+fYvgfMnzD8P8ms7QZ8SyQHow3D6/5/lW+QCMGuWXuyN1qjh66Hwhpv2/WFkdcw248xvc9h+f8AKsW9tzbXkkXQA8fSvS/Cel/2do6F1xNP+8f29B+X86qvU5YaEU43kb3auU8b6n9n05bJD+8uDlvZR/if611LsFUknAA5NeSa9qR1XVprgHMYOyMf7I6fn1/GuTDQ5p37G9WVlYzTSUUV6ZyBRRRQAUUUUAFFFFABS0lFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAwooooEFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUCgBaKKKQBSUtJTAKKKKACiiigApaKSgBaKSloASilpKACiiigAooooAWiiigBKKKKACiiigAopaTFABRRRQAUtJRQAUUUtACUtFFABRRRQAlFFFABRRRQAUUUtAGnoWonS9VhuMny87ZB6qev8Aj+FesKwZQwIIIyCK8VFek+D9T+3aSIHbMtt8h917H+n4VxYuGnMjooy6FvxJpY1TS2QD97Gd6f1H5VgRQLbxLEowFGBXbEZFctqUH2e6ZcfKeVrGlN25S5LW5h6zceTYsoPzSfKP61zFXtWuvtN6wU/u4/lX+pqhXoU1ZHNJ3YUUUVZIUUUUAFFFFABRRRQAUtJRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFACUUUUALRRRQAUUUUATWk5trmOUfwnJ+neuxQh1BByCMg1xFdLoV151v5DH54+nutY1Vpc0g+hqQaNFqWp27yLlYzucf3gOg/PFdqOBxWbpFt5Vv5pHzP0+laROBXnVZuTsdMYpanPeMdT+xaM0KNiW4+QY7L3P8AT8a8zrZ8Tan/AGnrErI2YYv3cf0HU/iaxq9ChDkgc1SV2JRRRW5mFFFFABRRRQAUUUUAFFFLQAlFFFABRRRQAUUUUAFFFFABRRRQMKKWkoEFFFFABRRRQAUUUUAFFFFABS0lLQAlFFFABRRRQAUUUUAFFFFABRRS0AFFFFIBKKKKYBRRRQAUUUUAFFFFABS0lFAC0lFFABRRS0AJRS0UAFJS0UAJRRRQAUUUUAFLmkooAKKKKACiiigApaSloAKSlooAKKKKAEooooAKKKKAFpKWkoAUVv8Ag+aePxBCkILLICsg/wBnufwrAr0HwPpnkWL37r+8n+VPZB/if5VhXklB3LppuR1lc/4sXbpDSx/65eFx6d66A1zesT+fcMmcog2j+tefSXvXOqex5tSVb1C2+y3jxgfL1X6VVr1k7o4mJRRRTAKKKKACiiigBaSlpKACiiigAooooAKKKKACiilxQAlFFFABRRRQAUUUUAJRS0UAFdL4X8IT+I1lmacW9tG23ft3Fm9AMj865qux8HeMINAt5bS7hkeB38xWjwSpxg8HtxWNdzUPc3NKai5e8ZnijwxN4cuYgZRNBMD5cm3ByOoIrBrpPGHigeI7qFYImitYM7A/3mJ6k+n0rm6dHn5Fz7inZS90KKKK1ICiiigAq9o5kGqwbMkbvm/3e/6VRrovD9qI42uWHzPwv0/z/KoqP3WVFanpSABAFGFAwPpWd4hlng0G7ktx+8CfkOhP4DJqbS7kT2oB+8nyn+lXZEWSNkdQysMEHuK8n4ZanZujxKitDW9PbTNVntiuFByh9VPT/PtWca9eLTV0cT0YUUUVQgooooAKKKKACiiigAooooAKKKKACiiigAopaSgAooooAKKKKACiiigAooooAKKKKBhRRRQIKWkooAKKKKBhRRRQIKKKKACiiigAoopaACiiigANFFFIBKKU0lMAooooAKKKKACiiigAooooAKKKKAClpKWgAoopKAClpKWgAooooASilpKACiiloASilpKACilpKACilpMUALRRRQAUUUUAJRRRQMKKKKACgUtKBQIt6bYvqOoQ2qdZGwT6Dufyr16CFLeCOGNdqIoVR6AVx/gbTNscuoSLyTsj/qf6fnXZ15uJnzS5TqoxsrlDW9RTS9KmuWxuA2oPVj0/x/CuYhmW4hWUdHGap+OtT+0X0djGf3cAy/ux/wABVXQLjdbNAx5jOR9D/k1pTpWhzEud5WJNctRJbeco+aPr9K5qu2lUSRMjDhgQa4qRGjlZG6qSDXRSeljOohtFFFbGYUUUUAFFFFABRS0lABRRRQAUUUtABSUtFABRRQaAEooooAKKKKACiiigBRQaVFLsFVSzMcAAZJNd1pPw1urq2WbULr7KWGREi7mA9znA/WsqlWNP4mXGDlscHRXba38Or3T4GuLGb7XGoyybcOB7DvXFcjg06dSNRXixShKO4lFFFaEhRS0lABRRQKAJIITPPHEvVmxXZRRiKJUUYVRgCsDQYN9y0x6IOPqf8muhPArnqvWxrBdSWx1dLDVreCRsJP8AKx9PQ/nXZZrxu+ufPvZJAeAcL9BXpvh3Uv7T0eGZjmVfkk+o/wARg1zV6VkpGtOd7oy/G2mfadOW9jX95bn5vdD/AIHH6155ivapolmheJ1DI6lWB7g15Fqlg+m6jNauD8jfKfVex/KtcLO65WRVjrco0UppK7DAKKKKAFFFJRQAUUUUALRRRQAlFFFABS0lLQAUlLSUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAtFFAoAKKKKQBSUUUwCiiigAooooAKKKKACiiigAooooAKWiigBKKDRQAUUUUALRRRQAUlFFABS0lLQ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JPAAptdr4N0Ddt1S6T5R/qFPf/a/wrOpNQV2VGLk7Gz4Z0IaVaebMo+1Sj5v9kf3f8a36MYrnfFeu/2XYmCB8XcwwuOqL3b/AA/+tXme9VmdekImN4x18TyNplq/7tD++YfxN/d+g/nXGmgkk5PNJXp04KEbI5JS5ncKKKK0JCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWiigApKKKACiiigAooooAKKKKACiiigApaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiigAoopaAEooooAKKKKAClFJSigYUUtJSEJRRRTAKKKKACiiigAooooAKWkpaACiiigApKWigBKKKXFABRRRQAUlLRQAlFFFAC0UUUAJRRRQAtFFFABSUtJQAUUUUALRRRQAlLRRQAUUUUAFFFFACGilooASilxSUAFLSUtABSUtJQAUUUUAFFFFABRRRQB1HhDxB9guPsVy+LaU/Kx6I3+B/z3r0YHNeI16F4Q8Qfa4Rp9y37+Mfu2J++o7fUfy/GuHE0ftI6KU/ss19f0WPWbAxkATplon9D6fQ15ZPDJbyvFKhR0OGU9Qa9orlfF+gfbLc39sn+kRj94oH319fqP89qjD1uV8r2HVhfVHnlJQaK9E5gooooAKKKKAClpKu6Zp02qX0drCOWPLdlHcmk3ZXY0rmn4Y0A6veebMp+yRHLn++f7v+NenIqogVQFUDAAHSq9jZQ6fZx20C7Y0GB6k9yfc1NNNHBC8srBY0UszHsBXl1ajqSOuEeVFPWNUg0mwe5mPThF7s3YV5PfX02oXkl1O2ZJDk+g9APar3iHW5Nb1Aycrbx/LEh7D1Puaya7aFLkV3uc9SfMwoooroMwooooAKKKKACiiigAooooAKKWigBKKWigBKKWigBKKKKACiiigApaSloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApRSUooAXNJRRSASig0UwCiiigAooooAKKKKACilooASiiigAooooAKWkpaACiiigApKWigAooooAKSiigApaTFLQAUUUlAC0lLSUAFLSUtABRRRQAlLRRQAUUUUAFFFFABRRRQAUlLRQAUUUUAFFFFACUUtJQAUUUUAFFFFABUkE0lvMk0TlJEIZWHY1HS0PXQD1fQNYj1iwWUYEyfLKnof8DWseRXkWkanNpN8lzFyBw6Z4Yeleq2V7Df2kdzA26Nxke3sfevLr0uSV1sddOfMjgvF3h/7DP9utk/0aQ/Oo/gb/A/57Vy1e1XFvFdW7wTIHjcbWU9xXlOu6PLo9+0LAmJuYn/ALy/4104atzLle5lVhZ3Rl0UUV1mIUopKUUASRxPLIqIpZmOAB1Jr0/w5oaaPZDcAbmQZlb09h7CsjwhoIijXUrlf3jD9ypHQf3v8K7GvPxNa/uo6aULasWuB8Y+IPtMjabav+5Q/vWH8bDt9B/OtrxXr39mWn2aB/8ASphwR1RfX/D/AOtXmxOTTw1G/vsVWp0Q2iiiu85wooooAKKKKACiiigApaKKAEpaKSgBaKSigAzS0lFAC0UlFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lFABRRRQAUUUUAFFFFAwooooAKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQMKWkpRQIKKKKQBSUUUwCiiigApaSloASilpKAFopKWgApKKKACiiigApaSigBaSlpKAFooooAKSlpKACiiigApaSigAooooAWkpaKAEpaSloAKKDSUALRRRQAUhpaSgAoopaACiiigAooooAKTNLRQAUUlFAC0lFFAC0UUlAC0UlLQAUUUUAKDXQ+F9eOl3fkTt/osp+Yn+A+v8AjXO0ucGonBTVmVF2d0e2AggEdDWbrekRaxYPbvhZB80b/wB1v8KwPBuv+co0y5f51H7lj3H938K7GvLlF05nWmpo8Wubaa0uHgnQpIhwwNRV6J4x0L7bam/t0/0iEfOAPvr/AIj/AD2rzzFelSqKcbnLOPKxK6XwroH9p3X2m4X/AESI8g/8tG9Pp61iafYT6nfR2sC5ZzyeyjuTXrllaRWNnFbQriONdo/xrLEVeVWW5VKF3dk4AAwBgVS1fU4tJ097mXkjhVHVm7CrrMFUsxAAGST2rzzxHq39raa86f6hL3y4uOqhOv4nJrjpQ55a7G85cqOdvbyW/vJLmdsySHJ9vYe1QUmaK9VKysjjCiiimAUUUUAFFFFABRRSmgBKKWkoAWkoooAWkopaAEopaSgAooooAKKKKACilooASilooASiiigAooooAKKKKACiig0ALSUUUAFFFGKACiiigAooooGFFFFAgooooAKKKKBhRS0UCEopaKAEooooAKKKKACiiigAoFFAoAWiiikAlFFFMAooooAKKKWgApKWkoAKKBS0AJRRRQAUUUUAFFFFABRS0UAFFFFABRRRQAUUUUAJRRRQAtFFFABRRRQAUlFFABRRRQAtFIKWgAooooAKKKKACiiigAooooAKKKKACkpaSgAopaKAEoNFFAC0UUUAFJS0lABRRRQA+KV4ZUkjYq6EMpHUEV6xoOrprOmJOMCVfllUdm/wPWvJK0dF1ifRb0TxfMh4kjJwHH+PvWFelzx03NKc+V6nrxGaxLvwjpF5cGZ4XRm6iJ9oP4Vb0zWbLVoRJbSgtj5o24ZfqK0M15t5QfY6rKRTsNJstMjK2kCx56t1Y/UmrmKXsSeAOp9Kx9U1u3it3js9R09bnpmWcYX34zk0kpSYNqKMHxr4h8hG0u1f94w/fMOw/u/4/wD66wfs2fAouCcAX4UD1+Sp4/Cct9MXOs6W0kjZZjd7iSe/Su+/4Q23PhBdGaf5gfN8/HHmdc49O30rq9pCkkkY8kp3Z49ilrprzwc9oxB1nSj7NcbT+VZE+kvDnN/pz4/uXIP9K7I1Iy2Zi4tbmfRQRtYjIOO4OaK0JCiiigAooooAKKKKBhRRRQIKKWkoAKKKKADNLSUtABSUUUAFFFFAC0UZooAKSlooAKSlpKAFpKKKACiiigAooooAKKKKAFopKM0AFFFFABRRRQAUUUUAFFLSUAFFFFABS0UhoAWikpaAEopaSgAooooAKKKKACloooAKKU9aSkAlFFFMAooooAKKKKAFpKKKAClpKKACiiigAoopaAEopaKACiiigApKKKAFopKWgApKWkoAKKWigBKKXFFACUUUUAFFFFAC0UUUAFFFFABSUtFABRRRQAUUUUAFFFFABRRRQAmaKWkoAWikpaACiiigAooooAKSlooASilooASiiigByO8bh0Yqw6EHBFaKeIdXjXauo3GPd8/zrMoqXFPdDu0W7nVL68GLm8nlH915CR+XSqmaKKaSWwXErul164/4VkyNKTL532QN32dcf988VwtannH/AIRXyc8fbt2P+2dZVYKVr9yoSauZmaM0UVqQFFFFMAooooAKKKWgBKKWigAooooAKSlpKACiiloASiiigAooooAKKKKACiijFAC0UlLQAUlLSUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lLQAUUUUAJRRRQAUtJS0AJRS0lABRRRQAUUUUAFFFFAC0UUUAFFFFIBKKKKYBRRRQAUUUUAFFLRQAlFFFABRRRQAtFFJQAtFFFABSUUUAFFFLQAlFLRQAUlLSUAFLRRQAUlFFABRRRQAUUUUAFLSUUAFLRRQAlFLRQAUUUUAFFFJQAtFFFABRRRQAUlFFABRS0UAFFFIaADNFFFABRRRQAZopaKAEoopaAEopaSgAooooAKm84fYvIwc+Zv8A0x/jUNFABRRRQAUUUUAFFFFABRRmigAozRS0AFFFFABSUtJQAtJRRQAtJS0UAJRRRQAUUUUAFFLSUAFLSUUAFFFFABRRRQAUUUUAFFFFABRRRQMKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lFABRRRQAUUUUAFFFLQAUUUUAFFFFIBKKKKYBRRRQAUUUUALSUUtACUUUUAFFLSUAFFFFAC0UlLQAlFFFABRRS0AFFFFACUUtFABSUtJQAUUtJQAUUUUAFFFFABRRRQAtFFFABRRSUALRSUtABSUtJQAtFIKWgAoopKACiiigAooooAWkoooAKKKKACiiigBaQ0UGgApaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiloAKSiigAooooAKKKKACiiloAKKKKAEpaKKACkoooAKKWigBKKKKACiiigAopaSgAooooAKKKKACiiigAooooAKWkpaACkpaKAEopaKAEooooAWkopaAEopaSgAooooAKKKKACiiigAooooAKUUlKKACiiikAlFFFMAooooAKKKKACiiigAoopaACkpaSgAopaSgAooooAWkoooAKWkpaACiiigAooooAKSlpKACiiigAopaKACkpaKAEooooAKWiigBKKWkoAKWiigApKWigBKWiigApKWkoAKKKKACiiigApaKKACiiigAooooAKSlooASilpKAClpKWgApKWigBKKWkNABRRRQAUUUtACUUUUDCiilxQISlpKKACiiigAopaSgAooooAKWkFLQAUUUUAJS0UUAFFFFACUUUUAFFLSUAFFFFABRRRigAooooAKKKKACiiigAooooAKWikNABRRRQAUUUUAFFLRQAUUUUAJRRRQAUUUUAFFFFABRRRQAUtJSigAooopAFJRRTAKKKKACiiigAooooAKKKKACiiigBaSlooASiiigAooooAKKKKACiiigAooooAKKKKACiiigAopaKACiiigBKWiigBKWkpaACiiigAooooAKKKKACiiigAooooASilpKAClpKKAFooooAKKKKACiiigAozRRQAlFLSUAFFLRQAUUUUAFJS0lABRRS0AFFFFABSGlooASlpKWgBKKWigBKKKKACilooASiiloABRSUtABSUUtACUuaSigBc0lFFABS0UlAC0UZooASiiigAzS0lFABRRRQAUUUUAFFFFABRRRQMWkoooEFFFFABRRRQAtFFJQAUUUUAFFFFABRRRQAUUUUAFFFFABS0UUAFFFFIBKKWkpgFFFFABRRRQAYopaSgAopcUlABQKKKAFpKWigBKKKWgBKKKKACiiloASiiigAopaSgAooooAKKKKACiiigBaKKKACiiigApKWigApKWigBKKKKACloooAKKKKAEpaKKACkpaKAEopaKAAUUUUAFFFFABRRRQAUUUUAJRS0UAFFFFABRRRQAUUUUAFFFFABRRSUALSUUUAFFFFAC0lLSUALSUtJQAtFFFACGloooAKSlooASilooASilooASlpKKAFpKKKACiiigAooooAKKKWgBKKKKACiiigAooooAKKKKACiiigAopaKAEooooAKKKKACiiigAooooAKKKKACiiigAooooAWikpaACiiikMSilpKYgooooAKKWigBKKKKACiiloAKSlpKAFopKKAFooooASiiigBaKKKAA0lLSUAFFFFABRRRQAUUUYoAKWiigBKWiigAooooAKKKKACigUUAFJilpKAFooooAKKKKACkpaSgApaSloAKKSigBaKKKACiiigAooooAKKKKAEooooAWkpaKACiiigAooooAKKSloAKSlpKAFpKKKACiiloAKKKKACkpaKACkpaSgBaSiigBaSlpKAFopKKAFoopDQAUUUUAFFFFABRRS0AJRRRQAUUUUALSUtFACUUUUAFFFFABRRRQAUtJRQAtJRRQAUUUUAFFFFABRRRQAtJS0lABRRRQAUUUUAFFFFABS0UUAFFLSUgCkoopgFFFFAC0lLSUAFFFFABRRS0AJRS0lABRRRQAtFJS0AFJS0UAFJS0UAFJS0UAJRS0UAFJS0lAC0UlLQAUUUUAJmlpKKAFooooAKKKKACiiigApKKKAFpKWigBKKWigBKKKWgAooooAQ0UUUALSUUYoAKWiigAooooAKKKQ0AFFFLQAUUUlAC5pKKKACiiigAooooAWiiigApKWkoAKKKWgAooooAKSlooAKSlpKACiiloASloooAKSlooAKKKKAEooooAKKKKACiiigBaSlpKACiiigAooooAKKKKACiiigAooooAKKKKBhRRRQIKKKKACilooASiiigAooooAKKKKACiiigApRSUUALRRRQAUUUUAIaKDRQAUUUUAFAoooAWkpaSgAoFFFAC0UUUAFFJRQAUUUUAFLSUtABRRRQAUlLRQAlLmkooAKKKWgBKWiigAooooASloooAKKKKACiiigAooooAKKKKACiikoAWiiigAooooAKKKKAEopaSgBaKSloAKKKKACiiigAooooAKSlooAKSlooASloooAKKKKAEoFFLQAUUUUAFJQaKAFpKWkoAKWig0AFFJS0AFJQaKACiiigApc0lFAC0UlLQAUUUUAJRS0UAJS0UUAFFFFACUUtJQAtJRRQAUUUUAFFLSUAFFFLQAUUUhoAKKKKACiiigAooooAWikooAKKKKACiiigAooooAKKKKAClpKKAFooooAKKKKQAaSg0UwCiiigYUUUUCCiiloASilooAKKKSgAooooAWkoooAKWkpaAEpaKKACiiigBKKWkoAKWkpaACkNLRQAlFFLQAlFLRQAlFLRQAUUlLQAUUUUAFFFFABRRRQAUUUUAFFFFACUtFFABSUtFACUUuKSgBaKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApKWigBKKKKACilooAKKKKAEopaKAEpaKKAEopaSgApaSloAKKKKACkpaKACiiigAooooASiiigBaSiloASiiigAooooAKKKKAFooooAKSlooASiiigAopcUUAJRS0lABRRRQAUUtFACUUUUAFFFFABRRRQAUUUUAFLSUtABRRRQAhopaSgAooooAKKWigBKKWkoAKWkooAKKKKACilooASiiigAoopaACiiigAooooAKSlooASiiigAooooAKKKKAFpKKKACijFFABRRRQAtFFFABSUUUALRRSUALRSUtABRRRQAUUUUAFJS0UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAJS0lLQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFJS0lAC0lFFAC0UUUAFFFFABRRRQAUUUUAFFJS0AJRS0lABRRRQAUUCloASilooASilooASilxRQAUUUUAFJS0UAFFFFABSGlpKACiiigAooooAKKKKAFpKKKACiiloASijFLQAUUUUAFFKaSgApKKKACiiigBaKKKACkpaKAEopaKAEooooAKKKKACiiigApaSloAKKKKACkpaSgApaSloASiiigAopaKAEopaKAEpaSigBaSlooASilooASilooAKMUUUAFJS0UAJRS0YoASlpKKAFooooAKSlooAKKKSgBaSiigApaSloAKSlooAKKKKACiiigApKWigAooooAKKKKACiiigAooooAKKKKADNFJS0AFFFFABRRRQAUmKWigApKWigBKWkooAWkzRRQAtFFFACUUtFACUUtJQAUUUUAFFFFABRRRQAtFJS0AFJRRQAUUUUAFFFFABS0lFABRRRQAUUUtACUUUUAFFFFABRRRQAUUUUALRRRQAUCilFACUUUUgEopaSmAUUUUAFGaKKAClpKWgAooooAKSlooASilooASlxRRQAlLSUtABRRRQAUlLRQAlFLSUAFFLiigBKWiigAoopKACiiigApaSigBaKKKACiiigAooooAKKKKACkpaKACiiigAooooAKSlooAKKKKACiiigBKKWigAooooAKKKKACiiigAooooAKKKKACiiigBKWiigAooooASiiigAooooAWkpaTFABRRRQAUtJS0AFFFFACUUtFABRRRQAUUUUAFJRRQAUtJS0AFFFFACUUUUAFFLSUAFLSUtACUUUtACUUUUAFFFFAwooooEFFFFABRRRQAUUUUAFFKKKAEooooAKKKKAClpKWgAopKUUAFJRRSGFFFFMQUUUUAFFFFAC0lFFABS0lFABRRRQAtFJRQAtFJS0AFFFFABSUtFABSUtJQAUUUUALmkoooAWiikoAKKKKACiiigBaSiigAFLSUtABRRRQAUZopKAFooooAKKKKACiiigAooooAKKKKACiiigAooooAKSlooASloooAKSlpKADNFFFABRRS0AFJS0UAFFFFABRRRQAUUUUABpKKWgBKKWigAooooAKSiigBaSlpDQAUtJS0AFFFJQAtFFFABRRRQAlFFFABRRS0AFJRRQAUUUUAFFFLQAlFLSUAFFFFABRRRQAUUUtACUUtJQAUUUUDCiiigQUUUUALSUtJQAUUUUAFFFFABRRRQAUtFFAAetFFFIBKKKKYBRRRQAUUUUAFFFFABRRRQAUUtFACUUtJQAUUUUAFFFFABRRRQAUUtFACUUUtABRRRQAlFFFABRRRQAUUUUDFpKKKBBS0UUAFFFFABSUtIaAClpKWgAooozQAUUUlAC0UUUAJS0lFAC0lFLQAUUUlAC0UUUAIaWkooAWkpaKAEooooAKWkpaACiiigAoopKAFopKKAFpKWigAFFFFACUUtFABRRSUALRSCloAKKKKACikzS0AFJS0lABRRRQAtFJRQAUUUtABSUtIaACiiigAooooAKKKKACiiigAooooAKKKKAClpKWgBKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKAA0UlLQAlFFFABRRRQAUUUUAFFFFABRRRQAtJRRQAUUUUAFFFFABRRRQMKKKKBC0UlFABRRRQAUUUUAFFFFABRRRQAtJS0lABRRRQAUtJRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAxaSiigBaQ0UUCCiiigYUtJRQAtFJS0CEooooAXNJRRQMKKKKACg0UUCCiiigYtFFFAhKKKKACiiigYUUUUCCiiigAooooAKKKKACiiigBaSiigApaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiigApaSigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClpKKAFopKWkAlFFGKYBRRRigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiijFABRRS0AJRRRQAUUUUAFFFFABRRRQAUUtFACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUuKAEooooAKKKKACiiigAoopaAEpaKKAEpaKKAEopaKAEopaTFABRS4ooASiiigAooooAM0UUUAFFFFAwooooEFLRRQAlFLRQAlFFFABRRRQAUUUtACUUtJQAUUUUAFLSUtACUUtJQAUUUUAFFFFABRRS0AJRRRQAUUUtACUUUtACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFLQAlFLRQAlKKSnYpDP/9k="; -export const faviconContentType = "image/jpeg"; + "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"; +export const faviconContentType = "image/png"; diff --git a/bunny_js/test/llm.test.ts b/bunny_js/test/llm.test.ts index 4cbbd0673..66f7e0cc3 100644 --- a/bunny_js/test/llm.test.ts +++ b/bunny_js/test/llm.test.ts @@ -431,11 +431,11 @@ describe("sc_llm_x402", () => { it("serves the favicon as raw bytes on GET /favicon.png", async () => { const res = await handleLLMRequest(makeEvent({ httpMethod: "GET", path: "/favicon.png" })); expect(res.status).toBe(200); - expect(res.headers.get("content-type")).toBe("image/jpeg"); + expect(res.headers.get("content-type")).toBe("image/png"); const bytes = new Uint8Array(await res.arrayBuffer()); expect(bytes.length).toBeGreaterThan(0); - expect(bytes[0]).toBe(0xff); - expect(bytes[1]).toBe(0xd8); + expect(bytes[0]).toBe(0x89); + expect(bytes[1]).toBe(0x50); }); it("answers a HEAD /favicon.png probe with 200 and an image content-type", async () => { @@ -444,7 +444,7 @@ describe("sc_llm_x402", () => { {}, ); expect(res.statusCode).toBe(200); - expect(res.headers["content-type"]).toBe("image/jpeg"); + expect(res.headers["content-type"]).toBe("image/png"); expect(res.body).toBe(""); }); diff --git a/bunny_js/test/routes.test.ts b/bunny_js/test/routes.test.ts index 81657d648..8b38d0838 100644 --- a/bunny_js/test/routes.test.ts +++ b/bunny_js/test/routes.test.ts @@ -578,7 +578,7 @@ describe("discovery", () => { const res = await handleRequest(discoveryRequest("GET", "favicon.png")); expect(res.status).toBe(200); - expect(res.headers.get("content-type")).toBe("image/jpeg"); + expect(res.headers.get("content-type")).toBe("image/png"); expect((await res.arrayBuffer()).byteLength).toBeGreaterThan(0); }); diff --git a/scw_js/discovery.ts b/scw_js/discovery.ts index af0e5fc48..f08885767 100644 --- a/scw_js/discovery.ts +++ b/scw_js/discovery.ts @@ -43,7 +43,7 @@ export function buildRootPage(input: RootPageInput): string { - + ${escapeHtml(input.title)} diff --git a/scw_js/favicon.ts b/scw_js/favicon.ts index 6693e3ff5..922b31605 100644 --- a/scw_js/favicon.ts +++ b/scw_js/favicon.ts @@ -1,3 +1,3 @@ export const faviconBase64 = - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAQABAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDLopTSV9EeSJS0UUxBSUtFABRRRQAUUUUAJRS4ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACkpaKAEopaKACilpKACiiigAoopcUAJRRRQAUUUUAFJS0UAFFFFACUUtFABRRRQAlFLRQAUUUUAFJS0UAFFFFABRRRQAlLRRQAUUUUAFFFFABRRRQAlFLRQAUUUUAFFFFABRRRQAlFLRQAUUUUAJS0lLQAUUUUAFFFFABRRRQAUUUUAFJS0UAJRS0UAFFFFACUtFFACUUtFACUtFFABRRRQAUUUUAJRS0UAJS0UUAFFFFABRRRQAUUUUAFJS0UAJRS0UAJRS0UAFFFKKQAaSlNFACUUUUwCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKWgBKKWkoAKKKKACiiigAooooAWkoooAKKKWgBKKKKAFpKWg0AJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUUtACUUGigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWiigAooopABpKKKYBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFGaKKAFpKKKACiiigAooooAKKKWgApKWigBKKKKACilooAKQ0tJQAUUUUAFFFFABRRRQAUUUUAFFFLQAlFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFLQAlLRRQAlFFFABRS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUooAKSlNFIBKKKKYBRRRQAUUUUAFFFFABRRRQAUUUtABSUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUtJQAUUUtABRRRQAUUUUAFJS0lABRRS0AJS0UUAJRRRigAopaMUAJRS0UAJRRRQAUUUUAFFFFAAKWiigBKKKWgBKKKKACiiigAoopcUAFJS0lABRRS0AJRS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAAKWikoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaSloAKKKKQCUUUUwCiiigAooooAKKKKAClpKWgBKKKKACiiigAooooAKKKKACiiloASilpKACilooASilpDQAUUUUAFBopaAEopaKAEopaKAEopaSgBaSlooAKSlpKACiiigApaSloAKKSloAM0UlLQAUlLSUAFLSUtABSUtJQAUUUUAFFFFABRRRQMKKKKAFooooEFJS0lAC0UUUAFJS0lABRRRQAUtJRQAUUUUALSUtIaACiiigApaKKACkpaKAEooooAWikooAKKKKACiiigAooooAKKKKAClpKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWkooAKKKKACiiigAooooAKKKWgBKWilFACUUUUgEooopgFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJRQAtJS0lABRRRQAZooooAKKKKACiiigApaSigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACilooASloooAKSlpKACiiigAooooAKKWigBKKWkoAWikpaACkpaSgAooooAKKKKACiiigAooooAKWkpaACkpaKAEooooGFLSUUCCiiigAooooAKKWigBKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooASloooAKKKKACiiigBaKSloAKKKKQCUUUUwCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKBhRRRQIKKWkoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAopaKAEooooAWkoooAWkpaMUAJRS0UAJRRRQAtFJS0AFFFFACUUUUAFFLRQAlFFFABRRRQAUUUUAFFFFABS0lFAC0lLRQAlFFFABRRRQAUUUYoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKWkNABRRRQAUUUUAFFFFABRRRQAUtJSigBKWiikAlFFFMAooooAKKKKACiiigAooooAKWkpaACiikoAWkpaSgAopcUlABS0lLQAlFFFABRRRQAUUUUAFFGKKACiiigAooooAKKKKACilooASloooASilooASiiigAoopaACiiigAopKKACilpKACiiigYtFJS0hBSGlooASiilpgJRRRQAUUUUAFFFFAC0UUlAC0lLSUAFFFFABRRRQAUUUUAFFFFAC0lFFABRRRQAUUUtACUtJRQAtJS0lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALSUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lKKACiiikAlFLSUwCiiigAooooAKKKKACiiigAooooAKKKKACiiigBaKKKAEopaSgAooooAKKKKACiiigBaSiigAooooAKKKKACiiloAKKKKACiiigAooooASiiigAopaKAEoopaAEopaSgBaSiigAooooGFLSUtAgpKKKACjNFFABRRRQAUUUUAFFFFAC0GiigBKKKKACiiigAooooAKKWjFACUUUUAFFFFABRRRQMKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRS0AFFFFIAopKKYBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAC0lFLQAlFLSUALmkoooAKKKKACiiigAooooAKWkooAWkpaKAEopaSgAooooAWiiigAooooAKKKKAEooooAWiiigBKWkooAWkpaSgAooooAKKWigBKKWkNABRRRQAUUUYoAKKWigBKWkpaACkoooAWkoooAKKKKACiiigAooooAWikooAWkpaSgAopaSgAooooAKKWkoAKKKKACiiigAoopaACkpaSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClpKUUAFFFFIBKKKKYBRRRQAopKWkoAKKKKACiiigAooooAKKKKAClpKKACiiigAooooAKKKKACiiigAooooAKKKKAFopKXNABSUtJQAUUUUAFFFFABRRRQAUUUUAFFLSUAFFFFABRRRQAUUUUAFLRRQAUUUlAC0lLRQAlLRRQAUUUGgBKKKWgBKKKKACiiigYUUuKSgQUUUUAFFFFABRRRQAUUUUAFFBooAWkpaQ0AFFFFABRRRQAUUUUAFFFFABRRRQAUUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAwpwpKKBBRRRSASiiimAUUUUAFFFFABRRRQAUUUUAFLSUtABSUUUAFFLRQAlFFFABRRRQAUUUUAFFFFABRRS0AFFFFACUUtJQAtJS0lABRRRQAUUUUALRSUtACUUUUAFFFFABRS0lABRRS0AJRRRQAtFFFAAaSlpKAFooooASilpKACiiloASilpKACilpKBhRRRQIWkoooAKWkooAKKKKAFopKWgAoopDQAUUUUALRSUtACUUtJQAUUUUDCiiigQUtFFABSUtJQAtJRRQAUUUUAFFLSUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AFFFFIBKKWkpgFFFFABRRRQAUUtJQAUUUUAFFFFABRRS0AFFFJQAUUUUAFLSUUAFFFFABRS0UAJRS0hoAWikpaAEooooAUUUUUAFJS0UAJRSmkoAKKKKACiiigYUUUtAgooooAKKKKACiiigAooooAKSlooAKSlooASilpKACiiloASiiigBaSiigAopaKAEooooAKKKKAFpKWkoAKKKKAFpKKKAFoNFFACUtJRQAtJS0UAJRS0mKACilooASiiigBaSiigAooooAKKKKAFpDRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AFFFFIBKKKWmAlFFFABRRRQAUUUtACUUUUAFFLSUAFLSUUALSUtJQAUUUUAFFFFABRRRQAUUUUAGaKKKAClpKKACiiigAopaKACiikoAKKKWgAooooAKKKKACiikNAC0UlLQAUUlFAC0lLRQAUUUUAFFFJQAtFFFABSUUUALRSZpc0AJRRRQAUtAooAKKKKACkpaQ0ALSUUUAFFFFABRRRQAUUUtABRRSUAFFFFABRRRQAtFJS0AIaKKKACiiloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKACiiikAUlFFMAooooAKKKKACilooASiiigAooooAWiiigAoNFFACUUUUAFFFFABQaKWgBKKKKACiiigAooooAKWkooAKKKKAFpKKKACiiigBaSlooAKQ0tJQAUUUUALSUUtACUUUUALRRRQAUlLRQAUlFFAC0lLRQAlFFFABRRRQAUUUUAFFFFAC0UUUAJRRRQAUUUUAFFFFABRRRQAUtJS0AFJS0UAJRS0lABRRRQAUtFIaACiiigAooooAKKKKACiiigApaSloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAFpKKKACiiigAooooAKKKKACiiigAooooAKWkooGLRRRQIKKKKQCUUUUwCiiigAooooAKKKKACiiigAoopaACikpaACg0UlABRRSigBKKWigBKKWkoAKKKKACiiigAooooAKKKKACiiigAooooAKKKWgAooooAKKKKAEopaSgAooooAKKKKACiiigBaKKKAEopaSgBRRSUUAFFLSUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFLRQAlFFFABRRRQMWkpaKBBSUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUALSUUUAFFFFABRS0UAJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUtJS0AKaSlNJSAKDRRTASiiigAooooAKKBRQAUUUUAFLRRQAUlLSUAFFFFABS0lFAC0UlFAC0lLQaAEooooAKKKKACiiigAooooAKKKKACiiloASlopKAClpKKAFopKKAFpKKWgBBS0UUAJRRRQAUUUUALRRRQAUlLSUAFFFFABRRRQAUUlFADiCMZBGelJV/UohFbaaQPv2oY/99tVCkndXGFFFFMQUUUUAFFFFABRRRQAtJRRQAUUUUALSUUtACUuaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKWigBKWkpaAA0UUUgCiiimAlFFLQAlFFFAC0lFFABRRRQAUUUUAFFFFABRRS0AFJS0lABRRRQAtFJS0AJRRS0AJRRRQAtJRRQAUUUUAFFFFABS0lLQAUlLRQAlFFFABRRS0AJS0UUAFFFJQAUUUUAFFFFAC0lFFABRRRQAUUUUAFFFSRxGVtoZAf9tgo/M0AR0YrSh0SefBF1YID/fu0H9a2NO8ET3s6q2q6YoPa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iigAooooAKKKKAFpKKKACiiigAooooAWkoooAKKKKACiiigBaSiloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAopaSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWkpaACkpaKQCUUUUwCiiigAooooAKKKKACilpKACiiigAooooAKKWigBKWkpaACg0UlABRRS0AJRS0lABRRRQAUUtFABSUtFACUUUtABSUUUDCiiigQUUUUALQaSigApaSjNABRRRQAUUUUAFFFFABRRRQAUUUUAFKBSYq3p9jNqV7HawDLuevYDuTSbsrsaVzT8NaEdYvN0gItYiDIf7x7KK9PjRY41RFCqowAOgFVtO0+HTLKO1gGEQcnux7k1YmlSCF5ZGCogLMx6AV5dWo6kjrhHlRT1bVIdJsXuZiMjhEzyzdhXlF9ezahdyXM7bpHOT7ew9qu+INak1rUDJysCfLEnoPU+5rJrtoUeRXe5z1J8zCiiiugzCiiigAooooAKKKKAOq8Ia/8AYbgWNy3+jyn5WJ+43+B/z3r0QHNeIivQ/CGv/bIBY3T5njHyMTy6/wCI/wA964cTRt7yOilPozT8Q6JHrNkVAC3CZMT+/ofY15ZLG8MjRyIUdSQynqD6V7XXI+MdBFxC2pWy/vYx+9UfxL6/UfyqcPWs+Vjqwvqjz+kzSmkr0DmFooooAKKKKACiiigAooooAKKKKACiiigAoopaAEopaSgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKAEooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAFoopRQAlJS0lIYUUUUxBRRRQAUUtFACUUUUAFFFFABRRiloAKSiigBaKSloAKSlooASiiigAooooAWiiigBKKKKAFoopKAFoNFFACUtFFACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUoFADkVnYKoJYnAA716d4a0JdIs98oBupRmQ/3f9kVj+DvD+Nup3Sf9cFI/8e/wrtOlefia13yo6KUOrFzXn/jHxAbqZtOtX/cRn96wP329PoP51t+Lde/s6zNrbvi6mGMjqi9z9fSvN8VWGo/bYVZ/ZQlFLRXcc4lFLilEbnorH8KLgNop/kyf883/AO+TS+RL/wA83/75NK4WI6KkMMg/5Zv/AN8mkMbjqjflRcLDKKUjFJTAKkhmkt5klicpIhDKwPINRUtFrgereHtaTWbDecLcR8SoPX1Hsa1yAwINeQaTqc+k3yXMB5HDKejDuDXrFjeRX9nFdQHMcgyPUex968yvS5HdbHXTlzKx574r0A6ddfarePFpKeg/gb0+lc3ivaLu1hvLWS3nQPFIMMK8p1rSZdH1B7d8sh5jfH3lrpw9bmXK9zGpDld0ZtFFLiuoyEooooAKKKKACiiigAooooAKKKKACiiigAopaSgAopaDQAlFFFAC0lFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFLSGgAooooAKKKKACiiigAooooAKKKKACiiigApaSloAKKKKQCUUGimAUUUUALRSUtACUUUUAFFFFAC0UlLQAlFFFAC0lFFABRRRQAUUUtACUUUUAFFFFABRRRQAUUtJQAUUUUAFFFFABRRS0AJRS0UAJRRilxQAlFFGKAClpMUuKACkpwVmOAM1Zi0y+n/ANVZzv8A7sZNJtIdmVKXFbEPhXWpuli6D1chf51oQ+BtUfmR7eP6uT/IVDqwXUahLscvijFdtD4AY/66/UeyR5/mavxeBNOTHmTXEn4hf6Vm8TTXUr2UjzrFLivUovCejRf8uYc+rux/rV2HRtNt/wDVWNup9fLBP61DxceiKVFnkUcMkpxGjOfRRmui8O+Gbi9vlku4JIrWP5m3qRv9hXo6oqDCqqj0UYpwrKeLbVki40UtwVQqhVAAAwAOgqnqF4tnbM/BkIIRT3P+FWpZVijZ3OFUZJrk728e7nMjZC9FX0FYQg5O5pJ2Vjn59Jub26e4uroNJIcsQuafHoFuv35JG/ECtTNIXVRlmAHvXYpPZGPKimukWSf8sc/ViamSytk+7BGP+AimyahaRnDTpn0Bz/Kq0mt2qfd3v9Fx/On77D3UaAjReiKPoKXGKx28QJ/DbsfqwFRN4gk/ht1H1YmjkkHNE3aK57+37jP+qi/Wj+37n/nnF+R/xp+zkLnR0X40Vzv9vXP/ADzi/WnL4gmH3oIz9CaPZyDnRvFVPUA/hTGtoH+9DGfqorIXxD/etvyb/wCtUya/bn70Ui/TBpckw5olxtNs3626fgMfyqu+i2bdEZfo3+NPTWLJ/wDlqV/3lNWUu7eT7k8bfRhR7yH7rMt/D8Z/1c7L/vDNbXhr7To1w8UsyyWkpyRyCh9f8aaGB6U8Gpm3JWY0kndHajBFZPiDRU1nT2iGFnT5onPY+n0NM0e/zi2kP+4T/KtquLWEro2+JHj02kajbE+bZXCgcZMZx+dVSpU4IIPvXteBTHghl/1kUb/7yg11LGd0YuieKYpcV65NoGkz/f0+3+qpt/lVGXwdo0mcQPH/ALkh/rmtFi4dSXRZ5hRXoE3gKybPk3Uyf7wDf4VQl8AXI/1N9E3s6Ef41osRTfUn2UjjqK6GbwTrMediQyj/AGZMfzxVKXw3rMWd2nzH/dAb+VWqsH1JcGjKpasS2F5B/rrSeP8A3oyKgIq00xWG5opcUYpiEooooAKKKKAFpKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAWkoooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBaKKBQAUUUUgCkpaSmAUUUUAFFFFABRRRQAUUUUAFFFLQAlFFFABRRRQAUUUUALRRQaAEoopaAEooooAKKKMUAFFLSUAFFFFABRRRQAUtJS0AFFFFACir2n6Te6pLstIGfH3m6Kv1NWvDuiSaze4OVt4sGRx/Ie5r1C2tobSBYYI1jjUYCqK5q1dQ0W5rCnzanEwfD+4YA3F7Gh9EQt+pxV6LwDZqR5t5M/sqhf8a66jNcbxNR9TdUonPReC9GjOWilk/wB+Q/0xV6Lw7o8P3NPhz/tDd/OtPNNZ0QZdlUepOKh1Kj6lcsURx2lvCMRQRIP9lAKlwKpTa1pcH+tv7ZcdvMBqhN4v0WIcXRkPpGhP9KFCpLoHNFG7ikNcnN4+slz5VrO/uxC/41Qm+IE5/wBTYxr7u5P8sVaw9R9CfaxR3VLmvNZfG2ryE7XhjHosef55qlN4k1efIa/lAPZML/LFaLCT6k+2ieqlgOScD3qvJqFnDnzLuBMf3pAK8ilu7iY/vZ5H/wB5yan06yN5Pkj5F5Y/0qvqiW7J9s3sj1D+2rAruWcOp6FQTmon162X7qu36VzYAVQAMAVm6te/ZofLQ/vXHHsPWkqEWynUaRb13xc88v2e3iURofmJOcmsGTWr1+kir/uqKoIjyuFRSzE4AAySa63SPh9ql8Flu8WcJ5w4y5H+72/Gul+zpLUyXPN6HMteXUn3p5D/AMCx/KkjguLpsRxyTMeyqWNevad4H0aw2s0BuJB/FOd36dK6CG3igUJFEiKOyqAK5p42K+FGscNJ/EzxKLwvrswyml3OP9pNv86n/wCEO8QYz/Zc35r/AI17ZtHpRgelZfXpdjT6tHueFS+GtagyZNLugB3EZb+VZ8tvLC22WN0b0ZSDX0IVHpUctvDOm2aGORfR1BH61Ucc+qJeFXRnzyRzRivZdQ8D6HfZItfs7n+KA7f06fpXHaz8Pb6xjeaxkF3EvO0DDgfTv+H5V008XTlpsYyoSicUaKDkHB60V1GIlLRRQAUZoooAek8sf3JGX6MRVhNUvE6Tsfrg1TopWQ7s1YtfvI2BOxseox/Kuz0nxYt5bK0sGJF4cKe/rXm9WbK7ezuBIvI6MvqKyqUYyWxcajR6qmt2jfeLp9V/wp41vTd2w3sKt6O20/rXKwypPEskZyrDINVdTsheQZUfvV5U+vtXJ7GN7G/O7HfRXEE4zFNHIP8AZYGpa8TbdGxxkMPwqxFqmoQf6q+uU+krY/nVPB9mR7buj2SlrymHxZrUP/L4XH+2in+laEPj3Uo8eZDbyj/dIP6GoeEmtilWiejUmBXEx/EEcebp/wD3xJ/iKvw+O9Lkx5kdxGfdQf5Gs3QqLoUqkWdPioJbG1nH722hk/3kBrNh8V6LMcC9VD6OpX+laEOp2Fx/qbyB/wDdkFRyziVeLKc3hrR5s77GMf7mV/kaozeCNIk+4s0Z/wBmTP8APNdGCGGRyPalqlVmuouSLOOk+H9sc+Xeyr/vID/hWRqHgrULSMyW7LcqOSFGG/LvXpFFXHEzT3JdKLPEGUqxVgQQcEHtSYr0vxN4Zi1OF7q2QLeKM8ceZ7H39682IKsVYEEHBB7V30qqqK5zzg4sbRRRWpAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFKKKAEooooAKKKKACiiigAooooAKKKKACiiigBaBSUtACUUUtACUUUUAFFFFABRRRQAUUUtABRRRQAUUUUAFJS0UAFJS0UAJRS0lAC0lApaAEopaSgAoopaACiiigAooooAKSlooASilpKACiiloAKWkpc0mB6v4asFsNCtkxh5FEr/AFbn+WB+Fa/So7fabeLb93YMfTFVtYMq6JfGHPmiByuOucHpXjyvKep3LSJy2t+N2huXttNRGCHa0z8gn2H9awZfF2tyD/j82/7kaj+lYQpa9ONGCWxyOpJl+XXdVn/1moXLD08wgfpVKSaSU5kdnP8AtMTTKK0UUtkTdiiiiiqEFFFFACilptKKAHxRPNIscYyzHAFdZZ2q2lusa8nqT6mqWkWHkR+fIP3jjgegrSkkWONnY4VRkmuepK+iNYRtqyG9uktLdpXPToPU+lY+l6TqHiTUCkCEknLyH7qD3NSadYXfizW1ihUrCvVj0RfU+9ex6XpdtpNilpaptRRyT1Y+p9656tZUlZbmkKbqO/QztA8J6foaK6J511jmZxz+A7Vv4opa86c3J3Z2KKirIMUUUVJQUUUUAFJVDUda07Sk3Xt3FD6KTlj9AOa43VPibCm5NMtDIe0kxwP++Rz+orWFGc/hREqkY7s789ay9W13TdJt3e7uo1YDiMEF2PsK8j1HxVrOpkia9dUP8ER2L+nX8axySSSeprshgXvJnPLEroiS7n+1Xs9xtC+bIz7R2yc1DRRXopWVjjeoUUUUwCiiigAooooASiiloA1NHvzbzCGQ/unPBP8ACa6XqK4aui0fUDNH5Mh+dBwfUVhUh1RpCXQr63YlT9pjHB++B/OsWu1cK6FWAKkYIPeuV1CzNpcFRny25U+1VTlfRhONinRRS1qZiUUYpaAEozS0lAE0d1cQnMc8iH/ZcirsXiHV4cbNQnx/tPu/nWZRUuEXuhptHQQ+NNZiILTJKB2eMc/liu60HXoNctS6L5c8eBJETnHuPUV5NXSeBfN/4SD5M7PJbf8ATjH64rnr0YcraNadR3sel15r4109bPWfOjGEuF38f3uh/wAfxr0uuI+IZG3T/wC9+8/9lrlwzaqI1qq8ThqKKK9Q5AooooAWkpaDQAlFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAtFFFABSUtJQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lLQAlFLRSASiiimAUUUUAFFFFABRRRQAtFJRmgBaKKDQAlFFFABRRRQAtFJS0AJRmiigAooooAKKKKAFzSUUtACUZpaKACiiigBKKKKAClpKWgAooooA9T8Kaj/aGhxbj+8hHlP+HQ/littgCpB5BFeZeENT/s/V1jc4huMRt7Hsfz4/GvTu1eVXhyTOunLmieRa/ph0vWJoAuIyd8f+6en+H4Vm16P4z0v7Xpv2uNcy2/J907/l1/OvOa76M+eJzzjysbRQaK2ICiiigAoopaADFaekWH2iYTSD90h/M1Rt4HuZ0iTqx/KuughS3gSJB8qjFZ1JW0LhG7HnpWNqE0t7eRadagu7sF2r1LHoKtapfC0tyFP71+FHp71u/DnQcs+tXC5PKQZ/8AHm/p+dc05KEXJmqXNLlR2Xh/Q4dC0uO2jCmQjMrgfeb/AA9K1qSlrypScndnckkrIKKKKQxKM1DdXcFnbvPcypFEgyzOcAV5v4i+IU0++20jMUfQ3DD5m+g7fz+la0qMqjtFETqRgtTtdZ8TaZoi/wClTgy4yIU5c/h2/GvP9Z+IWpX+6OyH2OE8ZU5c/j2/D86495HkdndyzsclmOST7mm5r06WEhDV6s4p15S2HSyyTSGSR2d25LMck0yiiuu1jAKKKKACiiigAooooAKKKKACiiigAooooAKfFI0MiyIcMpyDTKKQHW2V0t3Arjg9GHoaL60W7tyh+8OVPoa52wvDaThuSh4YV1SMHUMpBBGQa55LlZsnzI4x0aNyrDDA4INNrf1qyDRm5QcqPnHqPWsA1vGV0ZtWYUUlLVEhRRRQAUUUooAMV6N4J0v7HpZvHX97c8jPZB0/Pr+VcRo+nnU9TgtRnazfMR2Ucn9K9cjRY41RFCqoAUDsBXFip2XKb0Y3dx5NeY+MdSXUNaaOM5ith5QPq38R/Pj8K7nxDqf9laPNOpHmt8kX+8e/4da8mJJOTyfepwkNeZjrS6CUUUV3nOFFFFABRRRQAUUUUAFFFFABRS0lABRRRQAUUUUAFFFFABRRRQAUUUUALSUUUAFFLSUAFFFLQAlFFFABRRRQAUUUUAFFFFABS0lLQAUUUUgEooNFMAooooAWkoooAKKKKACiiigAooooAKKKKACiiloASilooASiiigYUUtFAhKKWigBKKWkoAWikooAKWiigBKKWkoAKKKKACiiigBwYjkcH2r1nw9qf9q6PDcMcygbJP8AeH+PX8a8lrp/BOqfZNTazkbEVz93PZx0/Pp+Vc2JhzRuuhrSlZnosiLIjI4BVgQQe4ryHV9PbTdUntTnajfKfVTyD+Vewdq4/wAc6b5trHfovzR/I/8Aunofz/nXLhp8srPqa1Y3VzgKKKK9M5QooooAKWirVhbfartUx8o5b6Um7AjZ0WzEUHnsPnkHHsK0JpFijZ3OFUZNPACqABgCsPW7ss4tkPA5b6+lc6vORvpFFXZLrGpRQoP3kzhEHpk17lY2cdhZQ2sQwkSBF+gFeW/Dyx+0+IjOwyttGXH+8eB/M161XFjJ3ly9jfDR05mOoooriOkKydc1+y0Kzaa5kBkI/dxKfmc+3t71B4k8SWvh+zLyEPcOP3UIPLe59BXjWo6ldapeyXd3IXlc/gB2A9AK68PhnU1lsc9Wty6Lcua54hvdeuvNuX2xj7kKk7U/+v71kk0lLXrRioqyOFtt3YlFFFUIKKKKACiiigAooooAKKKKAClNJRQAUUUUAFFFFABRRRQAVt6Le8/ZnP8Auf4ViU9HaNw6nBByCKmSurDTsztGAZSpAIIwRXJ6hafZLpkA+Q8r9K6Syuhd2ySDg9GHoah1W1FxaMQPnT5h/hWEHyysayV1c5Y0gpaK6TEKKKKAClpKsWdrJe3kVtEMvIwUUm7K4HbeBtN8u3k1CRfml+SP/dHU/n/Kuw7VDaWyWdpFbxDCRqFH4VBq+oLpmmT3bY+RflHqx4A/OvJm3UmdsVyxOF8a6mLvVRaRtmO2GD/vnr/QVzFK7tJI0jnc7EsxPcmkr1IRUYpHJJ3dwoooqyQooooAKKKKACiiigAooooAKKKKACigUtACUUUUAFLSUtABSUtJQAUUUUAFFFFAC0UlFABRRRQAUUUUAFFFFABRRRQAUUUUAFLRRQAUUUUgEooopgFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFLSUUALRRRQAUlLSYoAWkpaSgBaKKKAEooooAKKKKAFooooATNFFFABRRRQAUUUUALTo3aKRZEOGUggjsRTKWgD2HSb9NT0uC7XGXX5h6MOCPzqa8tkvLSW3k5SRSp/GuJ8C6n5dzLp8jfLKN8ef7w6j8R/Ku8ryaseSZ2RfNE8Yu7aSzu5beUYeNipqGuy8daXsmi1KNflfEcmPXsfy4/CuNr0qc+eKZyyVnYSiiitCRa6PRLXyrXzWHzSHP4dqwbaE3FxHEP4jiuwQKiBVGABgCsqktLGkF1I7mdbe3eVuij865J3aR2djlmOSa1teucslup4HzN/SsXNFONlcU3d2PUvhnahNKu7oj5pJdgPso/+vXc1y3w9QL4ThPdpHP64/pXU149d3qM9CkrQQZrI8Q+ILbQdPaeU7pWBEUQPLn/AA9TVzUtRt9LsZbu6cJHGMn1J7AepNeJa5rdxrupSXc/yqfljjzwi9hV4ah7SV3sTWqcqstytqOoXGqX0t3dOXlkOT6D0A9hVSiivZSSVkee3cKKKKYgooooAWikpc0AFJS5pKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDT0a68m58pj8knH0PaujPIrilJByDyK62yuBc2iSdyOfr3rCpHW5rB9DndStvs14wAwjfMtU66PWrYS2nmAfNGc/h3rnK0hK6IkrMKWkpRVkigV2fgXTd002oyLwn7uP69z/AE/GuPijaWRI0Us7EKoHcmvXtMsU07T4bVMfu1wT6nufzrlxM7Rsa0o3dy52rgfHep+bcxadG3yxfPJ/vHoPy/nXb3t3HY2c1zKcJEpY/wCFeO3dw93dS3Ehy8jFj+NYYWF5cz6GtaVlYhooor0TlCiiigAooooAKKKKACiiigAooooAKKKKACiiigAoopaAEooooAKKKKACiilNACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFLSUooAKKKKQCUUUUwCiiigAooooAKKKKAFopKWgApKWkoAKKWigBKKKKAFpKWigAooooAKSlFFABRSUtABSUtJQAUUUUALRSUtACUUUUAFFFFABRRRQAUUUooAmtbmS0uoriI4eNgy/UV7BZXUd9ZQ3UX3JUDD29q8aruvAup74JdOkPzR/vI8+h6j8/51yYqF48yNqMrOx1GpWKajp09pJjEi4B9D2P515BNC8ErxSKVdGKsD2Ir2mvPPG+m/ZtSW8RcR3A+b2cdfzH9aywtSz5WXWj1OUpRRSgV6BzGxoNvukknI+78q/XvW27BEZieAMmq+nW/wBnsIkIwxG4/U1DrM3lWDqD80hCj+tcz96RstInO3MxuLh5W6sajpKBXRsYnsXw+cP4ThA/hkcH/vquoJABJOAOprzb4e+IbSxhn069nWEM/mRO5wvTBGe3SrXjfxjAbN9M0yZZGlGJpozkKvdQfU/y/TyKlGUqzSR3wqJU0znPGfiQ65qXkwOfsUBIjx0c92/w9vrXMUlFepTgoR5UcUpOTuwooorQkKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAWkNFFABRRRQAtbOgz/NJAe43D+v9Kxans5zbXUcvYHn6VM1dDi7M62RQ6FSMgjBrj54jBM8Z/hYiuyHIB7Guf12DZcJMBw4wfqP8isabs7Gs1pcyaUUlOUFiABkmtzE6nwTpf2nUWvZBmO3+77uen5dfyr0TGKzNA03+zNGggYYkI3yf7x6/wCFaE8yW8Ek0hwkalmPsK8qtNzmdkFyxON8eaptSLTYzycSS49Ow/r+VcLVrUr2TUdRnu5PvSNnHoOw/KqtejShyRSOacrsKKKK1ICiiigAooooAKKKWgBKKKKACiiigAooooAKKKKACiiigAooooAKKWkoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoopRQMKKKKQhKKKKYBRRRQAUUUUAFFFGKACilooAKKSloAKKKSgAooooAWiiigBKKKKAClpKWgApKKKACiiigAooooAKKKKACiiloGJRS0UCEooooAKWkooAXNXNMvn07UYLpM5jbJHqO4/KqVKKTV1Yadnc9rhlSeFJY23I6hlPqDWdr+mDVNImgAzIBvjP+0On+H41k+CNT+06c1lIf3lufl91P+B/pXV15Mk6czsTUoniDAqxBGCOxq1p0H2m9jQj5c5b6Ctbxhpn2DWGlRcRXGXGOx7j+v403QLfbHJcEfe+Vf616XOnG6OTl96xs9q53XJvMuViHRB+proZGCIWJwAMmuOnlM07yH+Jiaikru5c3pYhopTSV0GQZooooAKKKKACiiigAooooAKKKWgBKKKKACiiigAooooAKKKKACiiigAooooAKKKKACkoooAWiiigAooooA6vS5/PsIyeoG0/hSarb+fYvgfMnzD8P8ms7QZ8SyQHow3D6/5/lW+QCMGuWXuyN1qjh66Hwhpv2/WFkdcw248xvc9h+f8AKsW9tzbXkkXQA8fSvS/Cel/2do6F1xNP+8f29B+X86qvU5YaEU43kb3auU8b6n9n05bJD+8uDlvZR/if611LsFUknAA5NeSa9qR1XVprgHMYOyMf7I6fn1/GuTDQ5p37G9WVlYzTSUUV6ZyBRRRQAUUUUAFFFFABS0lFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAwooooEFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUCgBaKKKQBSUtJTAKKKKACiiigApaKSgBaKSloASilpKACiiigAooooAWiiigBKKKKACiiigAopaTFABRRRQAUtJRQAUUUtACUtFFABRRRQAlFFFABRRRQAUUUtAGnoWonS9VhuMny87ZB6qev8Aj+FesKwZQwIIIyCK8VFek+D9T+3aSIHbMtt8h917H+n4VxYuGnMjooy6FvxJpY1TS2QD97Gd6f1H5VgRQLbxLEowFGBXbEZFctqUH2e6ZcfKeVrGlN25S5LW5h6zceTYsoPzSfKP61zFXtWuvtN6wU/u4/lX+pqhXoU1ZHNJ3YUUUVZIUUUUAFFFFABRRRQAUtJRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFACUUUUALRRRQAUUUUATWk5trmOUfwnJ+neuxQh1BByCMg1xFdLoV151v5DH54+nutY1Vpc0g+hqQaNFqWp27yLlYzucf3gOg/PFdqOBxWbpFt5Vv5pHzP0+laROBXnVZuTsdMYpanPeMdT+xaM0KNiW4+QY7L3P8AT8a8zrZ8Tan/AGnrErI2YYv3cf0HU/iaxq9ChDkgc1SV2JRRRW5mFFFFABRRRQAUUUUAFFFLQAlFFFABRRRQAUUUUAFFFFABRRRQMKKWkoEFFFFABRRRQAUUUUAFFFFABS0lLQAlFFFABRRRQAUUUUAFFFFABRRS0AFFFFIBKKKKYBRRRQAUUUUAFFFFABS0lFAC0lFFABRRS0AJRS0UAFJS0UAJRRRQAUUUUAFLmkooAKKKKACiiigApaSloAKSlooAKKKKAEooooAKKKKAFpKWkoAUVv8Ag+aePxBCkILLICsg/wBnufwrAr0HwPpnkWL37r+8n+VPZB/if5VhXklB3LppuR1lc/4sXbpDSx/65eFx6d66A1zesT+fcMmcog2j+tefSXvXOqex5tSVb1C2+y3jxgfL1X6VVr1k7o4mJRRRTAKKKKACiiigBaSlpKACiiigAooooAKKKKACiilxQAlFFFABRRRQAUUUUAJRS0UAFdL4X8IT+I1lmacW9tG23ft3Fm9AMj865qux8HeMINAt5bS7hkeB38xWjwSpxg8HtxWNdzUPc3NKai5e8ZnijwxN4cuYgZRNBMD5cm3ByOoIrBrpPGHigeI7qFYImitYM7A/3mJ6k+n0rm6dHn5Fz7inZS90KKKK1ICiiigAq9o5kGqwbMkbvm/3e/6VRrovD9qI42uWHzPwv0/z/KoqP3WVFanpSABAFGFAwPpWd4hlng0G7ktx+8CfkOhP4DJqbS7kT2oB+8nyn+lXZEWSNkdQysMEHuK8n4ZanZujxKitDW9PbTNVntiuFByh9VPT/PtWca9eLTV0cT0YUUUVQgooooAKKKKACiiigAooooAKKKKACiiigAopaSgAooooAKKKKACiiigAooooAKKKKBhRRRQIKWkooAKKKKBhRRRQIKKKKACiiigAoopaACiiigANFFFIBKKU0lMAooooAKKKKACiiigAooooAKKKKAClpKWgAoopKAClpKWgAooooASilpKACiiloASilpKACilpKACilpMUALRRRQAUUUUAJRRRQMKKKKACgUtKBQIt6bYvqOoQ2qdZGwT6Dufyr16CFLeCOGNdqIoVR6AVx/gbTNscuoSLyTsj/qf6fnXZ15uJnzS5TqoxsrlDW9RTS9KmuWxuA2oPVj0/x/CuYhmW4hWUdHGap+OtT+0X0djGf3cAy/ux/wABVXQLjdbNAx5jOR9D/k1pTpWhzEud5WJNctRJbeco+aPr9K5qu2lUSRMjDhgQa4qRGjlZG6qSDXRSeljOohtFFFbGYUUUUAFFFFABRS0lABRRRQAUUUtABSUtFABRRQaAEooooAKKKKACiiigBRQaVFLsFVSzMcAAZJNd1pPw1urq2WbULr7KWGREi7mA9znA/WsqlWNP4mXGDlscHRXba38Or3T4GuLGb7XGoyybcOB7DvXFcjg06dSNRXixShKO4lFFFaEhRS0lABRRQKAJIITPPHEvVmxXZRRiKJUUYVRgCsDQYN9y0x6IOPqf8muhPArnqvWxrBdSWx1dLDVreCRsJP8AKx9PQ/nXZZrxu+ufPvZJAeAcL9BXpvh3Uv7T0eGZjmVfkk+o/wARg1zV6VkpGtOd7oy/G2mfadOW9jX95bn5vdD/AIHH6155ivapolmheJ1DI6lWB7g15Fqlg+m6jNauD8jfKfVex/KtcLO65WRVjrco0UppK7DAKKKKAFFFJRQAUUUUALRRRQAlFFFABS0lLQAUlLSUAFFFFABRRRQAUUUUAFFFFABRRS0AJRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAtFFAoAKKKKQBSUUUwCiiigAooooAKKKKACiiigAooooAKWiigBKKDRQAUUUUALRRRQAUlFFABS0lLQ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JPAAptdr4N0Ddt1S6T5R/qFPf/a/wrOpNQV2VGLk7Gz4Z0IaVaebMo+1Sj5v9kf3f8a36MYrnfFeu/2XYmCB8XcwwuOqL3b/AA/+tXme9VmdekImN4x18TyNplq/7tD++YfxN/d+g/nXGmgkk5PNJXp04KEbI5JS5ncKKKK0JCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKWiigApKKKACiiigAooooAKKKKACiiigApaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiigAoopaAEooooAKKKKAClFJSigYUUtJSEJRRRTAKKKKACiiigAooooAKWkpaACiiigApKWigBKKKXFABRRRQAUlLRQAlFFFAC0UUUAJRRRQAtFFFABSUtJQAUUUUALRRRQAlLRRQAUUUUAFFFFACGilooASilxSUAFLSUtABSUtJQAUUUUAFFFFABRRRQB1HhDxB9guPsVy+LaU/Kx6I3+B/z3r0YHNeI16F4Q8Qfa4Rp9y37+Mfu2J++o7fUfy/GuHE0ftI6KU/ss19f0WPWbAxkATplon9D6fQ15ZPDJbyvFKhR0OGU9Qa9orlfF+gfbLc39sn+kRj94oH319fqP89qjD1uV8r2HVhfVHnlJQaK9E5gooooAKKKKAClpKu6Zp02qX0drCOWPLdlHcmk3ZXY0rmn4Y0A6veebMp+yRHLn++f7v+NenIqogVQFUDAAHSq9jZQ6fZx20C7Y0GB6k9yfc1NNNHBC8srBY0UszHsBXl1ajqSOuEeVFPWNUg0mwe5mPThF7s3YV5PfX02oXkl1O2ZJDk+g9APar3iHW5Nb1Aycrbx/LEh7D1Puaya7aFLkV3uc9SfMwoooroMwooooAKKKKACiiigAooooAKKWigBKKWigBKKWigBKKKKACiiigApaSloASiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApRSUooAXNJRRSASig0UwCiiigAooooAKKKKACilooASiiigAooooAKWkpaACiiigApKWigAooooAKSiigApaTFLQAUUUlAC0lLSUAFLSUtABRRRQAlLRRQAUUUUAFFFFABRRRQAUlLRQAUUUUAFFFFACUUtJQAUUUUAFFFFABUkE0lvMk0TlJEIZWHY1HS0PXQD1fQNYj1iwWUYEyfLKnof8DWseRXkWkanNpN8lzFyBw6Z4Yeleq2V7Df2kdzA26Nxke3sfevLr0uSV1sddOfMjgvF3h/7DP9utk/0aQ/Oo/gb/A/57Vy1e1XFvFdW7wTIHjcbWU9xXlOu6PLo9+0LAmJuYn/ALy/4104atzLle5lVhZ3Rl0UUV1mIUopKUUASRxPLIqIpZmOAB1Jr0/w5oaaPZDcAbmQZlb09h7CsjwhoIijXUrlf3jD9ypHQf3v8K7GvPxNa/uo6aULasWuB8Y+IPtMjabav+5Q/vWH8bDt9B/OtrxXr39mWn2aB/8ASphwR1RfX/D/AOtXmxOTTw1G/vsVWp0Q2iiiu85wooooAKKKKACiiigApaKKAEpaKSgBaKSigAzS0lFAC0UlFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lFABRRRQAUUUUAFFFFAwooooAKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQMKWkpRQIKKKKQBSUUUwCiiigApaSloASilpKAFopKWgApKKKACiiigApaSigBaSlpKAFooooAKSlpKACiiigApaSigAooooAWkpaKAEpaSloAKKDSUALRRRQAUhpaSgAoopaACiiigAooooAKTNLRQAUUlFAC0lFFAC0UUlAC0UlLQAUUUUAKDXQ+F9eOl3fkTt/osp+Yn+A+v8AjXO0ucGonBTVmVF2d0e2AggEdDWbrekRaxYPbvhZB80b/wB1v8KwPBuv+co0y5f51H7lj3H938K7GvLlF05nWmpo8Wubaa0uHgnQpIhwwNRV6J4x0L7bam/t0/0iEfOAPvr/AIj/AD2rzzFelSqKcbnLOPKxK6XwroH9p3X2m4X/AESI8g/8tG9Pp61iafYT6nfR2sC5ZzyeyjuTXrllaRWNnFbQriONdo/xrLEVeVWW5VKF3dk4AAwBgVS1fU4tJ097mXkjhVHVm7CrrMFUsxAAGST2rzzxHq39raa86f6hL3y4uOqhOv4nJrjpQ55a7G85cqOdvbyW/vJLmdsySHJ9vYe1QUmaK9VKysjjCiiimAUUUUAFFFFABRRSmgBKKWkoAWkoooAWkopaAEopaSgAooooAKKKKACilooASilooASiiigAooooAKKKKACiig0ALSUUUAFFFGKACiiigAooooGFFFFAgooooAKKKKBhRS0UCEopaKAEooooAKKKKACiiigAoFFAoAWiiikAlFFFMAooooAKKKWgApKWkoAKKBS0AJRRRQAUUUUAFFFFABRS0UAFFFFABRRRQAUUUUAJRRRQAtFFFABRRRQAUlFFABRRRQAtFIKWgAooooAKKKKACiiigAooooAKKKKACkpaSgAopaKAEoNFFAC0UUUAFJS0lABRRRQA+KV4ZUkjYq6EMpHUEV6xoOrprOmJOMCVfllUdm/wPWvJK0dF1ifRb0TxfMh4kjJwHH+PvWFelzx03NKc+V6nrxGaxLvwjpF5cGZ4XRm6iJ9oP4Vb0zWbLVoRJbSgtj5o24ZfqK0M15t5QfY6rKRTsNJstMjK2kCx56t1Y/UmrmKXsSeAOp9Kx9U1u3it3js9R09bnpmWcYX34zk0kpSYNqKMHxr4h8hG0u1f94w/fMOw/u/4/wD66wfs2fAouCcAX4UD1+Sp4/Cct9MXOs6W0kjZZjd7iSe/Su+/4Q23PhBdGaf5gfN8/HHmdc49O30rq9pCkkkY8kp3Z49ilrprzwc9oxB1nSj7NcbT+VZE+kvDnN/pz4/uXIP9K7I1Iy2Zi4tbmfRQRtYjIOO4OaK0JCiiigAooooAKKKKBhRRRQIKKWkoAKKKKADNLSUtABSUUUAFFFFAC0UZooAKSlooAKSlpKAFpKKKACiiigAooooAKKKKAFopKM0AFFFFABRRRQAUUUUAFFLSUAFFFFABS0UhoAWikpaAEopaSgAooooAKKKKACloooAKKU9aSkAlFFFMAooooAKKKKAFpKKKAClpKKACiiigAoopaAEopaKACiiigApKKKAFopKWgApKWkoAKKWigBKKXFFACUUUUAFFFFAC0UUUAFFFFABSUtFABRRRQAUUUUAFFFFABRRRQAmaKWkoAWikpaACiiigAooooAKSlooASilooASiiigByO8bh0Yqw6EHBFaKeIdXjXauo3GPd8/zrMoqXFPdDu0W7nVL68GLm8nlH915CR+XSqmaKKaSWwXErul164/4VkyNKTL532QN32dcf988VwtannH/AIRXyc8fbt2P+2dZVYKVr9yoSauZmaM0UVqQFFFFMAooooAKKKWgBKKWigAooooAKSlpKACiiloASiiigAooooAKKKKACiijFAC0UlLQAUlLSUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lLQAUUUUAJRRRQAUtJS0AJRS0lABRRRQAUUUUAFFFFAC0UUUAFFFFIBKKKKYBRRRQAUUUUAFFLRQAlFFFABRRRQAtFFJQAtFFFABSUUUAFFFLQAlFLRQAUlLSUAFLRRQAUlFFABRRRQAUUUUAFLSUUAFLRRQAlFLRQAUUUUAFFFJQAtFFFABRRRQAUlFFABRS0UAFFFIaADNFFFABRRRQAZopaKAEoopaAEopaSgAooooAKm84fYvIwc+Zv8A0x/jUNFABRRRQAUUUUAFFFFABRRmigAozRS0AFFFFABSUtJQAtJRRQAtJS0UAJRRRQAUUUUAFFLSUAFLSUUAFFFFABRRRQAUUUUAFFFFABRRRQMKKKKBBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABS0lFABRRRQAUUUUAFFFLQAUUUUAFFFFIBKKKKYBRRRQAUUUUALSUUtACUUUUAFFLSUAFFFFAC0UlLQAlFFFABRRS0AFFFFACUUtFABSUtJQAUUtJQAUUUUAFFFFABRRRQAtFFFABRRSUALRSUtABSUtJQAtFIKWgAoopKACiiigAooooAWkoooAKKKKACiiigBaQ0UGgApaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiloAKSiigAooooAKKKKACiiloAKKKKAEpaKKACkoooAKKWigBKKKKACiiigAopaSgAooooAKKKKACiiigAooooAKWkpaACkpaKAEopaKAEooooAWkopaAEopaSgAooooAKKKKACiiigAooooAKUUlKKACiiikAlFFFMAooooAKKKKACiiigAoopaACkpaSgAopaSgAooooAWkoooAKWkpaACiiigAooooAKSlpKACiiigAopaKACkpaKAEooooAKWiigBKKWkoAKWiigApKWigBKWiigApKWkoAKKKKACiiigApaKKACiiigAooooAKSlooASilpKAClpKWgApKWigBKKWkNABRRRQAUUUtACUUUUDCiilxQISlpKKACiiigAopaSgAooooAKWkFLQAUUUUAJS0UUAFFFFACUUUUAFFLSUAFFFFABRRRigAooooAKKKKACiiigAooooAKWikNABRRRQAUUUUAFFLRQAUUUUAJRRRQAUUUUAFFFFABRRRQAUtJSigAooopAFJRRTAKKKKACiiigAooooAKKKKACiiigBaSlooASiiigAooooAKKKKACiiigAooooAKKKKACiiigAopaKACiiigBKWiigBKWkpaACiiigAooooAKKKKACiiigAooooASilpKAClpKKAFooooAKKKKACiiigAozRRQAlFLSUAFFLRQAUUUUAFJS0lABRRS0AFFFFABSGlooASlpKWgBKKWigBKKKKACilooASiiloABRSUtABSUUtACUuaSigBc0lFFABS0UlAC0UZooASiiigAzS0lFABRRRQAUUUUAFFFFABRRRQMWkoooEFFFFABRRRQAtFFJQAUUUUAFFFFABRRRQAUUUUAFFFFABS0UUAFFFFIBKKWkpgFFFFABRRRQAYopaSgAopcUlABQKKKAFpKWigBKKKWgBKKKKACiiloASiiigAopaSgAooooAKKKKACiiigBaKKKACiiigApKWigApKWigBKKKKACloooAKKKKAEpaKKACkpaKAEopaKAAUUUUAFFFFABRRRQAUUUUAJRS0UAFFFFABRRRQAUUUUAFFFFABRRSUALSUUUAFFFFAC0lLSUALSUtJQAtFFFACGloooAKSlooASilooASilooASlpKKAFpKKKACiiigAooooAKKKWgBKKKKACiiigAooooAKKKKACiiigAopaKAEooooAKKKKACiiigAooooAKKKKACiiigAooooAWikpaACiiikMSilpKYgooooAKKWigBKKKKACiiloAKSlpKAFopKKAFooooASiiigBaKKKAA0lLSUAFFFFABRRRQAUUUYoAKWiigBKWiigAooooAKKKKACigUUAFJilpKAFooooAKKKKACkpaSgApaSloAKKSigBaKKKACiiigAooooAKKKKAEooooAWkpaKACiiigAooooAKKSloAKSlpKAFpKKKACiiloAKKKKACkpaKACkpaSgBaSiigBaSlpKAFopKKAFoopDQAUUUUAFFFFABRRS0AJRRRQAUUUUALSUtFACUUUUAFFFFABRRRQAUtJRQAtJRRQAUUUUAFFFFABRRRQAtJS0lABRRRQAUUUUAFFFFABS0UUAFFLSUgCkoopgFFFFAC0lLSUAFFFFABRRS0AJRS0lABRRRQAtFJS0AFJS0UAFJS0UAFJS0UAJRS0UAFJS0lAC0UlLQAUUUUAJmlpKKAFooooAKKKKACiiigApKKKAFpKWigBKKWigBKKKWgAooooAQ0UUUALSUUYoAKWiigAooooAKKKQ0AFFFLQAUUUlAC5pKKKACiiigAooooAWiiigApKWkoAKKKWgAooooAKSlooAKSlpKACiiloASloooAKSlooAKKKKAEooooAKKKKACiiigBaSlpKACiiigAooooAKKKKACiiigAooooAKKKKBhRRRQIKKKKACilooASiiigAooooAKKKKACiiigApRSUUALRRRQAUUUUAIaKDRQAUUUUAFAoooAWkpaSgAoFFFAC0UUUAFFJRQAUUUUAFLSUtABRRRQAUlLRQAlLmkooAKKKWgBKWiigAooooASloooAKKKKACiiigAooooAKKKKACiikoAWiiigAooooAKKKKAEopaSgBaKSloAKKKKACiiigAooooAKSlooAKSlooASloooAKKKKAEoFFLQAUUUUAFJQaKAFpKWkoAKWig0AFFJS0AFJQaKACiiigApc0lFAC0UlLQAUUUUAJRS0UAJS0UUAFFFFACUUtJQAtJRRQAUUUUAFFLSUAFFFLQAUUUhoAKKKKACiiigAooooAWikooAKKKKACiiigAooooAKKKKAClpKKAFooooAKKKKQAaSg0UwCiiigYUUUUCCiiloASilooAKKKSgAooooAWkoooAKWkpaAEpaKKACiiigBKKWkoAKWkpaACkNLRQAlFFLQAlFLRQAlFLRQAUUlLQAUUUUAFFFFABRRRQAUUUUAFFFFACUtFFABSUtFACUUuKSgBaKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApKWigBKKKKACilooAKKKKAEopaKAEpaKKAEopaSgApaSloAKKKKACkpaKACiiigAooooASiiigBaSiloASiiigAooooAKKKKAFooooAKSlooASiiigAopcUUAJRS0lABRRRQAUUtFACUUUUAFFFFABRRRQAUUUUAFLSUtABRRRQAhopaSgAooooAKKWigBKKWkoAKWkooAKKKKACilooASiiigAoopaACiiigAooooAKSlooASiiigAooooAKKKKAFpKKKACijFFABRRRQAtFFFABSUUUALRRSUALRSUtABRRRQAUUUUAFJS0UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAJS0lLQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFJS0lAC0lFFAC0UUUAFFFFABRRRQAUUUUAFFJS0AJRS0lABRRRQAUUCloASilooASilooASilxRQAUUUUAFJS0UAFFFFABSGlpKACiiigAooooAKKKKAFpKKKACiiloASijFLQAUUUUAFFKaSgApKKKACiiigBaKKKACkpaKAEopaKAEooooAKKKKACiiigApaSloAKKKKACkpaSgApaSloASiiigAopaKAEopaKAEpaSigBaSlooASilooASilooAKMUUUAFJS0UAJRS0YoASlpKKAFooooAKSlooAKKKSgBaSiigApaSloAKSlooAKKKKACiiigApKWigAooooAKKKKACiiigAooooAKKKKADNFJS0AFFFFABRRRQAUmKWigApKWigBKWkooAWkzRRQAtFFFACUUtFACUUtJQAUUUUAFFFFABRRRQAtFJS0AFJRRQAUUUUAFFFFABS0lFABRRRQAUUUtACUUUUAFFFFABRRRQAUUUUALRRRQAUCilFACUUUUgEopaSmAUUUUAFGaKKAClpKWgAooooAKSlooASilooASlxRRQAlLSUtABRRRQAUlLRQAlFLSUAFFLiigBKWiigAoopKACiiigApaSigBaKKKACiiigAooooAKKKKACkpaKACiiigAooooAKSlooAKKKKACiiigBKKWigAooooAKKKKACiiigAooooAKKKKACiiigBKWiigAooooASiiigAooooAWkpaTFABRRRQAUtJS0AFFFFACUUtFABRRRQAUUUUAFJRRQAUtJS0AFFFFACUUUUAFFLSUAFLSUtACUUUtACUUUUAFFFFAwooooEFFFFABRRRQAUUUUAFFKKKAEooooAKKKKAClpKWgAopKUUAFJRRSGFFFFMQUUUUAFFFFAC0lFFABS0lFABRRRQAtFJRQAtFJS0AFFFFABSUtFABSUtJQAUUUUALmkoooAWiikoAKKKKACiiigBaSiigAFLSUtABRRRQAUZopKAFooooAKKKKACiiigAooooAKKKKACiiigAooooAKSlooASloooAKSlpKADNFFFABRRS0AFJS0UAFFFFABRRRQAUUUUABpKKWgBKKWigAooooAKSiigBaSlpDQAUtJS0AFFFJQAtFFFABRRRQAlFFFABRRS0AFJRRQAUUUUAFFFLQAlFLSUAFFFFABRRRQAUUUtACUUtJQAUUUUDCiiigQUUUUALSUtJQAUUUUAFFFFABRRRQAUtFFAAetFFFIBKKKKYBRRRQAUUUUAFFFFABRRRQAUUtFACUUtJQAUUUUAFFFFABRRRQAUUtFACUUUtABRRRQAlFFFABRRRQAUUUUDFpKKKBBS0UUAFFFFABSUtIaAClpKWgAooozQAUUUlAC0UUUAJS0lFAC0lFLQAUUUlAC0UUUAIaWkooAWkpaKAEooooAKWkpaACiiigAoopKAFopKKAFpKWigAFFFFACUUtFABRRSUALRSCloAKKKKACikzS0AFJS0lABRRRQAtFJRQAUUUtABSUtIaACiiigAooooAKKKKACiiigAooooAKKKKAClpKWgBKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigApaKKAA0UlLQAlFFFABRRRQAUUUUAFFFFABRRRQAtJRRQAUUUUAFFFFABRRRQMKKKKBC0UlFABRRRQAUUUUAFFFFABRRRQAtJS0lABRRRQAUtJRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAxaSiigBaQ0UUCCiiigYUtJRQAtFJS0CEooooAXNJRRQMKKKKACg0UUCCiiigYtFFFAhKKKKACiiigYUUUUCCiiigAooooAKKKKACiiigBaSiigApaSigBaSiigAooooAKKKKACiiigAooooAKKKKACiiigApaSigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAClpKKAFopKWkAlFFGKYBRRRigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiijFABRRS0AJRRRQAUUUUAFFFFABRRRQAUUtFACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUuKAEooooAKKKKACiiigAoopaAEpaKKAEpaKKAEopaKAEopaTFABRS4ooASiiigAooooAM0UUUAFFFFAwooooEFLRRQAlFLRQAlFFFABRRRQAUUUtACUUtJQAUUUUAFLSUtACUUtJQAUUUUAFFFFABRRS0AJRRRQAUUUtACUUUtACUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFLQAlFLRQAlKKSnYpDP/9k="; -export const faviconContentType = "image/jpeg"; + "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"; +export const faviconContentType = "image/png"; diff --git a/scw_js/test/genimg_x402_token.test.ts b/scw_js/test/genimg_x402_token.test.ts index fe7524793..b03d98f4f 100644 --- a/scw_js/test/genimg_x402_token.test.ts +++ b/scw_js/test/genimg_x402_token.test.ts @@ -612,14 +612,14 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { const response = await handle(event, {}); expect(response.statusCode).toBe(200); - expect(response.headers["Content-Type"]).toBe("image/jpeg"); + expect(response.headers["Content-Type"]).toBe("image/png"); expect(response.headers["Access-Control-Allow-Origin"]).toBe("*"); expect(response.isBase64Encoded).toBe(true); - // Valid base64 that decodes to a JPEG (SOI marker 0xFFD8). + // Valid base64 that decodes to a PNG (signature 0x8950). const bytes = Buffer.from(response.body, "base64"); expect(bytes.length).toBeGreaterThan(0); - expect(bytes[0]).toBe(0xff); - expect(bytes[1]).toBe(0xd8); + expect(bytes[0]).toBe(0x89); + expect(bytes[1]).toBe(0x50); }); test("should answer a HEAD /favicon.png probe with 200 and an image content-type", async () => { @@ -633,7 +633,7 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { const response = await handle(event, {}); expect(response.statusCode).toBe(200); - expect(response.headers["Content-Type"]).toBe("image/jpeg"); + expect(response.headers["Content-Type"]).toBe("image/png"); expect(response.body).toBe(""); });