Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .agents/skills/x402/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ has a buyer calling `/verify`, it is wrong.
| ---------------------------- | ----------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Client / buyer** | `website/` — `/imagegen` and `/assistent` | GitHub Pages | `hooks/useX402ImageGeneration.ts`, `hooks/useX402Chat.ts` — both use `wrapFetchWithPayment` from `@x402/fetch` |
| **Resource server / seller** | `scw_js/` (image) + `bunny_js/` (llm, search, mcp) | Scaleway Function + Bunny Edge Scripts | image: `scw_js/genimg_x402_token.ts` → `imagegen-agent.fretchen.eu`; llm/search/mcp: `bunny_js/src/llm_routes.ts`, `searchapi_routes.ts`, `mcp_routes.ts` → `llm-agent` / `web-agent` / `mcp-agent.fretchen.eu` (shared ported `x402_server.ts`) |
| **Facilitator** | `x402_facilitator/` | Scaleway Functions | `x402_facilitator.ts` — `/verify`, `/settle`, `/supported` at `facilitator.fretchen.eu` |
| **Facilitator** | `bunny_js/src/facilitator/` | Bunny Edge Script (`fretchen-facilitator`, `service="facilitator"`) | `facilitator_routes.ts` — `/verify`, `/settle`, `/supported` at `facilitator.fretchen.eu` (moved from the retired Scaleway function; the weekly wallet-report cron in `x402_facilitator/` is all that remains there) |

Plus `scw_js/llm_x402_cron.ts` — a 12-hourly cron that claims and settles accumulated
batch-settlement channels. It is seller-side infrastructure, not a fourth role.
Expand All @@ -80,9 +80,11 @@ fallback (see `genimg_x402_token.ts`).

- **EIP-712 domain names differ by network.** Mainnet USDC is `"USD Coin"`, testnet is `"USDC"`.
The verified per-network table is in `scw_js/README.md` → _Adding New Networks_. Never guess one.
- **The batch-settlement recipient whitelist is OR logic** across a manual list, testnet-only test
wallets, and NFT-holder status — and it checks `payTo`, the recipient, not the payer. See
`x402_facilitator/README.md` → _Whitelist Architecture_.
- **The batch-settlement recipient gate is an allowance check**: the recipient (`payTo`, not the
payer) must have `approve()`d the facilitator's wallet for the fee in the settled token; the
only bypass is `BATCH_SETTLEMENT_TEST_WALLETS`, honored on testnets only (the old manual
allowlist + NFT-holder branch are retired). See `bunny_js/README.md` → _Facilitator: recipient
gating_.
- **The facilitator collects a fee** via `transferFrom(merchant, facilitator, fee)`, in the token
the payment settled in — so the merchant needs one `approve()` **per token** it takes (USDC, and
EURC on Base). Amounts live only in `website/pages/x402/sellers/`.
Expand All @@ -99,8 +101,9 @@ fallback (see `genimg_x402_token.ts`).
`findSettlementToken`, default currency EURe with EURC and USDC one toggle away; its
spend-control cap is per-token, `10^decimals`) all read it. `getStablecoins()` remains
USDC+EURC only — nothing should read it for EURe. See `scw_js/README.md` → _Stablecoins
and pricing_ and _Known EURe Domain Names_, and `x402_facilitator/README.md` → _EURe via
Permit2_.
and pricing_ and _Known EURe Domain Names_; the EURe-via-Permit2 implementation lives in
`bunny_js/src/facilitator/` (end-to-end walkthrough:
`bunny_js/notebooks/facilitator/x402_exact_permit2.ipynb`).

## Writing about x402 on the site

Expand Down
82 changes: 41 additions & 41 deletions .github/THREAT_MODEL.md

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions .github/workflows/deploy-bunny-js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ jobs:
- name: Deploy facilitator script to Bunny Edge Scripting
uses: BunnyWay/actions/deploy-script@931a74b9c45ea9f9f01435fbbf1649830e67d3b3 # 2026-10-12 main; a mutable ref here is code-exec with the deploy keys — bump deliberately
with:
# The facilitator twin (payment core ported from x402_facilitator/). Publishing does
# NOT make it live: facilitator.fretchen.eu keeps serving the Scaleway function
# until the DNS flip (x402_facilitator/bunny-port-implementation-plan.md). Same
# rule as the other steps: set both secrets in GitHub before merging, or this step
# fails (and does not block the twins' deploys above).
# The facilitator twin (payment core ported from x402_facilitator/). Live: the DNS
# flip happened, facilitator.fretchen.eu serves this script, and the retired Scaleway
# function is deleted. Same rule as the other steps: set both secrets in GitHub
# before merging, or this step fails (and does not block the twins' deploys above).
script_id: ${{ secrets.BUNNYFACILITATOR_SCRIPT_ID }}
deploy_key: ${{ secrets.BUNNYFACILITATOR_DEPLOY_KEY }}
file: "bunny_js/dist/facilitator.js"
30 changes: 15 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ everything.

Monorepo for a blockchain-based AI service platform. Each subdirectory is an independent npm package — there is no root `package.json` or workspace manager.

| Directory | Purpose | Further reading |
| --------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `website/` | Frontend (Vike SSR + React 19 + Panda CSS) — also the blog | [`README`](website/README.md) · [`AGENTS.md`](website/AGENTS.md) |
| `eth/` | Solidity smart contracts (Hardhat, Optimism L2) | [`README`](eth/README.md) · [`AGENTS.md`](eth/AGENTS.md) |
| `scw_js/` | Serverless backend (Scaleway Functions: image gen, channel cron, growth API) | [`README`](scw_js/README.md) · [`AGENTS.md`](scw_js/AGENTS.md) |
| `bunny_js/` | Bunny Edge Scripts: the x402 llm/search/mcp sellers (plus the scw twins' origin domains) | [`README`](bunny_js/README.md) · [`AGENTS.md`](bunny_js/AGENTS.md) |
| `x402_facilitator/` | EIP-3009 USDC payment facilitator (Scaleway Functions) | [`README`](x402_facilitator/README.md) · [`AGENTS.md`](x402_facilitator/AGENTS.md) |
| `comment_service/` | Blog comment backend (Scaleway Functions) | [`README`](comment_service/README.md) · [`AGENTS.md`](comment_service/AGENTS.md) |
| `analytics/` | Serverless pageview counter (Scaleway Functions, S3-backed) | [`README`](analytics/README.md) · [`AGENTS.md`](analytics/AGENTS.md) |
| `growth-agent/` | AI growth agent cron container (Python, LangGraph) | [`README`](growth-agent/README.md) · [`AGENTS.md`](growth-agent/AGENTS.md) |
| `observability/` | Log CLI + Loki alert rules for all Scaleway functions | [`README`](observability/README.md) · [`AGENTS.md`](observability/AGENTS.md) |
| `shared/chain-utils/` | Shared blockchain utility library (Viem peer dep) | [`README`](shared/chain-utils/README.md) · [`AGENTS.md`](shared/chain-utils/AGENTS.md) |
| `notebooks/` | Python Jupyter notebooks for analysis | [`README`](notebooks/README.md) |
| Directory | Purpose | Further reading |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `website/` | Frontend (Vike SSR + React 19 + Panda CSS) — also the blog | [`README`](website/README.md) · [`AGENTS.md`](website/AGENTS.md) |
| `eth/` | Solidity smart contracts (Hardhat, Optimism L2) | [`README`](eth/README.md) · [`AGENTS.md`](eth/AGENTS.md) |
| `scw_js/` | Serverless backend (Scaleway Functions: image gen, channel cron, growth API) | [`README`](scw_js/README.md) · [`AGENTS.md`](scw_js/AGENTS.md) |
| `bunny_js/` | Bunny Edge Scripts: the x402 llm/search/mcp **sellers** — plus the facilitator (`facilitator.fretchen.eu`) | [`README`](bunny_js/README.md) · [`AGENTS.md`](bunny_js/AGENTS.md) |
| `x402_facilitator/` | Weekly facilitator wallet report (Scaleway Functions) — the EIP-3009 facilitator itself moved to `bunny_js/src/facilitator/` | [`README`](x402_facilitator/README.md) · [`AGENTS.md`](x402_facilitator/AGENTS.md) |
| `comment_service/` | Blog comment backend (Scaleway Functions) | [`README`](comment_service/README.md) · [`AGENTS.md`](comment_service/AGENTS.md) |
| `analytics/` | Serverless pageview counter (Scaleway Functions, S3-backed) | [`README`](analytics/README.md) · [`AGENTS.md`](analytics/AGENTS.md) |
| `growth-agent/` | AI growth agent cron container (Python, LangGraph) | [`README`](growth-agent/README.md) · [`AGENTS.md`](growth-agent/AGENTS.md) |
| `observability/` | Log CLI + Loki alert rules for all Scaleway functions | [`README`](observability/README.md) · [`AGENTS.md`](observability/AGENTS.md) |
| `shared/chain-utils/` | Shared blockchain utility library (Viem peer dep) | [`README`](shared/chain-utils/README.md) · [`AGENTS.md`](shared/chain-utils/AGENTS.md) |
| `notebooks/` | Python Jupyter notebooks for analysis | [`README`](notebooks/README.md) |

## Commands

Expand Down Expand Up @@ -75,7 +75,7 @@ llm/web/MCP on the bunny_js Edge Scripts).
Three traps, each documented in detail where it belongs:

- **EIP-712 domain names differ by network** (mainnet USDC is `"USD Coin"`, testnet is `"USDC"`). Follow the checklist in [`scw_js/README.md`](scw_js/README.md) → _Adding New Networks_, which carries the verified per-network table.
- **The x402 recipient whitelist uses OR logic** across a manual list, testnet-only test wallets, and NFT-holder status. See [`x402_facilitator/README.md`](x402_facilitator/README.md) → _Whitelist Architecture_.
- **The x402 recipient "whitelist" is really an allowance gate**: both schemes require the recipient to have `approve()`d the facilitator's wallet for the fee **in the settled token**; the only bypass is `BATCH_SETTLEMENT_TEST_WALLETS`, honored on testnets only (the old manual allowlist and NFT-holder branch are retired). See [`bunny_js/README.md`](bunny_js/README.md) → _Facilitator: recipient gating_.
- **Generated OpenAPI specs must publish the strictness they actually enforce.** `scw_js` and `x402_facilitator` generate their `openapi.*.json` from Zod, and `z.object` renders as `additionalProperties: false` — so a schema that merely _strips_ unknown keys publishes a strictness its handler does not enforce. Use `z.looseObject` for a request the handler forwards or ignores extras on, `z.object` where the body is built field by field (every response, and `genimg`'s request, which really does reject unknown fields). Each package's `test/openapi_*_generation.test.ts` asserts this, because nothing else catches it — and it has shipped wrong: the facilitator's published `PaymentRequest` described a request that could not be paid with, since real payloads carry the very fields it declared forbidden.

## Security
Expand All @@ -87,7 +87,7 @@ See [`.github/THREAT_MODEL.md`](.github/THREAT_MODEL.md) for the full asset inve
- `CONTRACT_OWNER_PRIVATE_KEY` (Hardhat keystore) — dedicated EOA `0x1af51D…fBB20`, controls every upgradeable contract and owns the three ERC-8004 agent identities (Base). Never use it for anything else.
- `SEPOLIA_PRIVATE_KEY` (Hardhat keystore) — deployment/script signing key `0x073f26…`. Does NOT own contracts.
- Agent wallet `0xAAEBC1…` — backend-only, whitelisted on GenImNFTv4 via `authorizeAgentWallet()`.
- Facilitator wallet — stored as a Scaleway secret, receives USDC fees only.
- Facilitator wallet — stored as a `--secret` on the bunny facilitator script (the retired Scaleway facilitator function and its secret copy are deleted; `walletreportcron` still carries the key, only to derive the wallet address for the weekly report — a public-address env would remove that copy); receives fees in the settled tokens (USDC; EURC/EURe on Base). Its pull semantics: `bunny_js/README.md` → _Facilitator: the fee pull_.
- Receiver-authorizer key (`RECEIVER_AUTHORIZER_PRIVATE_KEY`, scw_js Scaleway secret) — signs batch-settlement channel-config and refund authorizations for the `/assistent` sellers; never sends transactions. Compromise means revenue denial (early refunds, stalled claims), not fund redirection.

Rules that apply everywhere:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ wired together by x402 USDC payments.
| `website/` | The site + blog (Vike SSR, React 19, Panda CSS) |
| `eth/` | NFT and payment contracts (Hardhat, UUPS proxies) |
| `scw_js/` | Scaleway Functions: image gen, channel cron, growth API |
| `bunny_js/` | Bunny Edge Scripts: the x402 llm/search/mcp sellers |
| `x402_facilitator/` | EIP-3009 USDC payment facilitator |
| `bunny_js/` | Bunny Edge Scripts: the x402 sellers — llm/search/mcp — plus the facilitator |
| `x402_facilitator/` | Weekly facilitator wallet report (facilitator itself moved to `bunny_js`) |
| `comment_service/` | Blog comments |
| `analytics/` | Pageview counter (S3-backed) |
| `growth-agent/` | AI growth agent (Python, LangGraph) |
Expand Down
4 changes: 2 additions & 2 deletions bunny_js/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# bunny_js — agent rules

Bunny Edge Scripts: the x402 llm/search/mcp **sellers** plus the ported **facilitator**
(the payment core's verbatim twin from `x402_facilitator/`, waiting on its cutover —
see [`x402_facilitator/bunny-port-implementation-plan.md`](../x402_facilitator/bunny-port-implementation-plan.md)).
(the payment core's verbatim twin from `x402_facilitator/` — cutover done, the retired
Scaleway function deleted; the move's plan is preserved in git history).
See [`README.md`](./README.md) for the architecture, commands, environment/secrets, known
limits and deploy paths, and [`SECURITY.md`](./SECURITY.md) for the security posture.

Expand Down
45 changes: 42 additions & 3 deletions bunny_js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,48 @@ Only `scw_js`'s `llmx402cron` remains, claiming the channels these twins charge.
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`. Its cutover plan is
[`x402_facilitator/bunny-port-implementation-plan.md`](../x402_facilitator/bunny-port-implementation-plan.md):
until its DNS flip the Scaleway facilitator remains the live one at `facilitator.fretchen.eu`.
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).

### Facilitator: recipient gating (the "whitelist architecture")

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).

### Facilitator: the fee pull right after the settlement

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`:

- 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

Expand Down
5 changes: 3 additions & 2 deletions bunny_js/notebooks/facilitator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
The facilitator's four scheme notebooks — replicated from
[`x402_facilitator/notebooks/`](../../../x402_facilitator/notebooks/README.md) (the
authoritative per-scheme flow descriptions live there) — exercising the bunny twin
end-to-end over HTTP. Together they are the exact Task 5 probe matrix of the port plan
([`bunny-port-implementation-plan.md`](../../../x402_facilitator/bunny-port-implementation-plan.md)):
end-to-end over HTTP. The batch-settlement notebooks were the port's Task 5 probe matrix
(the plan itself is preserved in git history as
`x402_facilitator/bunny-port-implementation-plan.md`):

| Notebook | Kernel | Tests what (short form) |
| ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ |
Expand Down
Loading
Loading