diff --git a/website/components/FacilitatorApproval.tsx b/website/components/FacilitatorApproval.tsx new file mode 100644 index 000000000..5829bac54 --- /dev/null +++ b/website/components/FacilitatorApproval.tsx @@ -0,0 +1,370 @@ +import React, { useEffect, useState } from "react"; +import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useSwitchChain } from "wagmi"; +import { formatUnits, parseUnits, type Address } from "viem"; +import { css } from "../styled-system/css"; +import { getUSDCConfig, fromCAIP2, type USDCConfig } from "@fretchen/chain-utils"; + +// Minimal ERC-20 ABI for allowance + approve +export const ERC20_ABI = [ + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ name: "", type: "uint256" }], + }, + { + name: "approve", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "", type: "bool" }], + }, +] as const; + +// Supported networks for approval +export const APPROVAL_NETWORKS = [ + { network: "eip155:10", label: "Optimism" }, + { network: "eip155:8453", label: "Base" }, +] as const; + +export const APPROVAL_NETWORKS_WITH_TESTNETS = [ + ...APPROVAL_NETWORKS, + { network: "eip155:11155420", label: "OP Sepolia" }, + { network: "eip155:84532", label: "Base Sepolia" }, +] as const; + +// Preset approval amounts +const PRESETS = [ + { label: "1 USDC", value: "1" }, + { label: "10 USDC", value: "10" }, +]; + +/** Resolve the USDC config for a CAIP-2 network string. Returns null if unsupported. */ +export function getNetworkUSDCConfig(network: string): USDCConfig | null { + try { + return getUSDCConfig(network); + } catch { + return null; + } +} + +// ─── Styles ────────────────────────────────────────────────────────────────── + +const container = css({ + border: "1px solid token(colors.border, #e5e7eb)", + borderRadius: "8px", + padding: "20px", + marginBottom: "6", + backgroundColor: "token(colors.codeBg, #f9fafb)", +}); + +const statusRow = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + flexWrap: "wrap", + gap: "8px", + marginBottom: "4", +}); + +const label = css({ + fontSize: "sm", + color: "#6b7280", + fontWeight: "medium", +}); + +const valueText = css({ + fontSize: "lg", + fontWeight: "semibold", +}); + +const approveRow = css({ + display: "flex", + alignItems: "center", + gap: "8px", + flexWrap: "wrap", +}); + +const presetButton = css({ + padding: "6px 12px", + fontSize: "sm", + borderRadius: "6px", + border: "1px solid token(colors.border, #d1d5db)", + backgroundColor: "white", + cursor: "pointer", + fontWeight: "medium", + transition: "all 0.15s", + _hover: { + backgroundColor: "#f3f4f6", + borderColor: "#9ca3af", + }, + _disabled: { + opacity: 0.5, + cursor: "not-allowed", + }, +}); + +const activeButton = css({ + backgroundColor: "#2563eb", + color: "white", + borderColor: "#2563eb", + _hover: { + backgroundColor: "#1d4ed8", + }, +}); + +const selectedNetworkButton = css({ + backgroundColor: "#1e293b", + color: "white", + borderColor: "#1e293b", + _hover: { + backgroundColor: "#334155", + }, +}); + +const txStatus = css({ + fontSize: "sm", + marginTop: "3", + padding: "8px 12px", + borderRadius: "6px", +}); + +const connectHint = css({ + fontSize: "sm", + color: "#6b7280", + textAlign: "center", + padding: "12px", +}); + +const networkRow = css({ + display: "flex", + alignItems: "center", + gap: "8px", + flexWrap: "wrap", + marginBottom: "4", +}); + +// ─── Component ─────────────────────────────────────────────────────────────── + +interface FacilitatorApprovalProps { + facilitatorAddress?: Address | null; + showTestnets?: boolean; +} + +export function FacilitatorApproval({ + facilitatorAddress: propAddress, + showTestnets = false, +}: FacilitatorApprovalProps) { + const { address, isConnected, chainId } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + const [facilitatorAddress, setFacilitatorAddress] = useState
(propAddress ?? null); + const [fetchError, setFetchError] = useStateCould not load facilitator address: {fetchError}
+Connect your wallet to check and manage your USDC approval for the facilitator.
+USDC is not available on the selected network.
++ Network: +
+Your current USDC approval on {usdcConfig.name}
++ {isReadingAllowance ? "Loading…" : `${formattedAllowance} USDC`} +
+Facilitator address
+{facilitatorAddress}
+
+ USDC on {usdcConfig.name}: {usdcConfig.address}
+
+ Approve USDC spending: +
++ A collection of experiments around decentralized AI services. Everything here runs on Optimism and Base — pay + per use with a wallet, no subscriptions, no accounts. +
+ +
+ Accept crypto payments on your API or website with zero integration complexity. This is an independent{" "}
+ x402 facilitator — it handles payment verification and on-chain
+ settlement so you don't have to. Status:
Three steps to accept x402 payments on your service:
+ +
+ When a client requests a paid resource without payment, respond with HTTP 402 and your payment requirements.
+ Replace 0xYourMerchantAddress with your wallet address and set amount to your
+ price in USDC (6 decimals — 100000 = $0.10).
+
+ {`// HTTP 402 response body:
+{
+ "x402Version": 2,
+ "accepts": [{
+ "scheme": "exact",
+ "network": "eip155:10",
+ "amount": "70000",
+ "asset": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
+ "payTo": "0xYourMerchantAddress",
+ "maxTimeoutSeconds": 60,
+ "extra": { "name": "USD Coin", "version": "2" }
+ }],
+ "facilitatorUrl": "https://facilitator.fretchen.eu"
+}`}
+
+
+ The facilitator collects a 0.01 USDC fee per settlement via ERC-20 transferFrom. You need a
+ one-time USDC approval. Connect your seller wallet below to check your current approval and set it:
+
+ When a client sends a request with a PAYMENT-SIGNATURE header, verify the payment before
+ delivering the resource, then settle it on-chain:
+
+ {`// 1. Verify payment (before delivering resource)
+const verifyRes = await fetch("https://facilitator.fretchen.eu/verify", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ x402Version: 2, scheme: "exact",
+ network: "eip155:10", payload, details })
+});
+const { valid } = await verifyRes.json();
+if (!valid) return new Response("Payment invalid", { status: 402 });
+
+// 2. Deliver your resource
+const result = await generateImage(prompt);
+
+// 3. Settle payment (after successful delivery)
+await fetch("https://facilitator.fretchen.eu/settle", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ x402Version: 2, scheme: "exact",
+ network: "eip155:10", payload, details })
+});
+
+return new Response(JSON.stringify(result), { status: 200 });`}
+
+ + That's it — your service now accepts crypto payments. See the{" "} + agent onboarding guide for a complete walkthrough. +
+
+ The facilitator charges a flat 0.01 USDC per settlement, collected post-settlement via ERC-20{" "}
+ transferFrom. There is no percentage fee, no monthly minimum, no hidden costs.
+
| Your price | +Facilitator fee | +Effective rate | +Stripe (2.9% + $0.30) | +
|---|---|---|---|
| $0.07 | +$0.01 | +14.3% | +impossible (below minimum) | +
| $0.50 | +$0.01 | +2.0% | +$0.31 (62.9%) | +
| $1.00 | +$0.01 | +1.0% | +$0.33 (32.9%) | +
| $10.00 | +$0.01 | +0.1% | +$0.59 (5.9%) | +
+ The flat-fee model is especially competitive for micropayments — exactly the range where traditional payment + processors are prohibitively expensive or unavailable. +
+ +
+ The fee amount and facilitator address are advertised in the /supported endpoint under the{" "}
+ facilitator_fee extension.
+
+ x402 implements the long-dormant{" "} + HTTP 402 Payment Required{" "} + status code. A resource server (you) responds with payment requirements, the client signs a payment, and the + facilitator handles verification and on-chain settlement. +
+ +Key properties:
+
+ The facilitator at facilitator.fretchen.eu exposes three endpoints:
+
+ Validates a signed payment off-chain. Checks signature validity, sufficient balance, correct recipient, and + expiration. Call this before delivering your resource. +
+
+ {`curl -X POST https://facilitator.fretchen.eu/verify \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "x402Version": 2,
+ "scheme": "exact",
+ "network": "eip155:10",
+ "payload": "",
+ "details": {
+ "scheme": "exact",
+ "network": "eip155:10",
+ "amount": "100000",
+ "asset": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
+ "payTo": "0xYourMerchantAddress"
+ }
+ }'`}
+
+
+ Response: {`{ "valid": true }`} or {`{ "valid": false, "invalidReason": "..." }`}
+
+ Executes the payment on-chain via EIP-3009 transferWithAuthorization. Call this{" "}
+ after successful verification and resource delivery.
+
+ {`curl -X POST https://facilitator.fretchen.eu/settle \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "x402Version": 2,
+ "scheme": "exact",
+ "network": "eip155:10",
+ "payload": "",
+ "details": {
+ "scheme": "exact",
+ "network": "eip155:10",
+ "amount": "100000",
+ "asset": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
+ "payTo": "0xYourMerchantAddress"
+ }
+ }'`}
+
+
+ Response: {`{ "success": true, "txHash": "0x..." }`}
+
Returns supported networks, payment schemes, and fee configuration.
+
+ {`curl https://facilitator.fretchen.eu/supported`}
+
+
+ Returns a JSON object with kinds (supported network/scheme pairs), extensions (fee
+ configuration), and signers (facilitator addresses per network).
+
+ The facilitator supports the exact scheme with ERC-20 tokens (USDC) via{" "}
+ EIP-3009 transferWithAuthorization. The
+ buyer signs an off-chain authorization — no gas required from the buyer. The facilitator submits the
+ transaction on-chain.
+
+ Using the official @x402/fetch SDK, a client can pay for any x402 resource automatically:
+
+ {`import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
+import { registerExactEvmScheme } from "@x402/evm/exact/client";
+import { privateKeyToAccount } from "viem/accounts";
+
+const signer = privateKeyToAccount(\`0x\${PRIVATE_KEY}\`);
+const client = new x402Client();
+registerExactEvmScheme(client, { signer });
+
+const fetchWithPayment = wrapFetchWithPayment(fetch, client);
+
+// Payment is handled automatically on 402 response
+const response = await fetchWithPayment(
+ "https://imagegen-agent.fretchen.eu/genimg",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ prompt: "A futuristic cityscape" }),
+ }
+);
+
+const result = await response.json();
+console.log("Image:", result.imageUrl);
+console.log("NFT:", result.tokenId);`}
+
+
+ Full example of a Node.js endpoint protected by x402. Adapt the resource generation to your use case:
+
+ {`// Express / Node.js example
+app.post("/api/resource", async (req, res) => {
+ const paymentHeader = req.headers["payment-signature"];
+
+ // No payment → return 402 with requirements
+ if (!paymentHeader) {
+ return res.status(402).json({
+ x402Version: 2,
+ accepts: [{
+ scheme: "exact",
+ network: "eip155:10",
+ amount: "70000", // 0.07 USDC
+ asset: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
+ payTo: "0xYourMerchantAddress",
+ maxTimeoutSeconds: 60,
+ extra: { name: "USD Coin", version: "2" }
+ }],
+ facilitatorUrl: "https://facilitator.fretchen.eu"
+ });
+ }
+
+ // Verify payment
+ const payload = paymentHeader;
+ const details = { scheme: "exact", network: "eip155:10",
+ amount: "70000",
+ asset: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
+ payTo: "0xYourMerchantAddress" };
+
+ const verifyRes = await fetch("https://facilitator.fretchen.eu/verify", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ x402Version: 2, scheme: "exact",
+ network: "eip155:10", payload, details })
+ });
+
+ const { valid, invalidReason } = await verifyRes.json();
+ if (!valid) return res.status(402).json({ error: invalidReason });
+
+ // Deliver resource
+ const result = await generateYourResource(req.body);
+
+ // Settle payment
+ await fetch("https://facilitator.fretchen.eu/settle", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ x402Version: 2, scheme: "exact",
+ network: "eip155:10", payload, details })
+ });
+
+ return res.json(result);
+});`}
+
+
+ {/* ── 7. Supported networks ────────────────────────────────────── */}
+
+ | Network | +Chain ID | +USDC address | +Environment | +
|---|---|---|---|
| Optimism | +eip155:10 | +
+ 0x0b2C…Ff85
+ |
+ Production | +
| Base | +eip155:8453 | +
+ 0x8335…2913
+ |
+ Production | +
| OP Sepolia | +eip155:11155420 | +
+ 0x5fd8…30D7
+ |
+ Testnet | +
| Base Sepolia | +eip155:84532 | +
+ 0x036C…CF7e
+ |
+ Testnet | +
+ All wallets that support WalletConnect work — MetaMask, Coinbase Wallet, Rainbow, and others. Your customers + need a small amount of USDC on any supported network. +
+ + {/* ── 8. For your customers ────────────────────────────────────── */} + +When a user interacts with your x402-protected service, the payment flow is invisible and instant:
++ Each payment is individually signed via EIP-3009. The + authorization is bound to a specific amount, recipient, and expiration. The protocol never has blanket access + to your customer's funds. See the AI Image Generator for a live example. +
+ + {/* ── 9. Links ─────────────────────────────────────────────────── */} + +