Skip to content

Instantly share code, notes, and snippets.

@erikzrekz
Created March 2, 2026 18:59
Show Gist options
  • Select an option

  • Save erikzrekz/e7d6e436927620ecb79ca81048ae335f to your computer and use it in GitHub Desktop.

Select an option

Save erikzrekz/e7d6e436927620ecb79ca81048ae335f to your computer and use it in GitHub Desktop.
Songs of Myself x402 Demo
import {
createPublicClient,
createWalletClient,
defineChain,
getAddress,
hexToBigInt,
http,
isHex,
parseAbi,
recoverTypedDataAddress,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
interface Env {
R2_BUCKET: R2Bucket;
PAYMENT_ADDRESS?: string;
// SBC / Radius config
PRICE_SBC?: string;
SBC_ASSET?: string;
TOKEN_DECIMALS?: string;
SBC_EIP712_NAME?: string;
SBC_EIP712_VERSION?: string;
NETWORK_NAME?: string;
NETWORK_RPC?: string;
NETWORK_CHAIN_ID?: string;
PROTECTED_PATHS?: string;
// Radius fixed-fee API endpoint (mainnet dashboard)
TRANSACTION_COST_API_URL?: string;
// Settlement signer (required for settlement)
SETTLEMENT_PRIVATE_KEY?: string;
// Backward-compatible fallback
PRICE_USDC?: string;
}
const DEFAULT_PRICE_SBC = "0.10";
const DEFAULT_NETWORK_NAME = "radius";
const DEFAULT_RADIUS_MAINNET_RPC = "https://rpc.radiustech.xyz";
const DEFAULT_RADIUS_MAINNET_CHAIN_ID = 723;
const DEFAULT_SBC_DECIMALS = 18;
const DEFAULT_MAX_TIMEOUT_SECONDS = 300;
const RPC_RETRY_MAX_ATTEMPTS = 3;
const RPC_RETRY_BASE_DELAY_MS = 250;
const DEFAULT_TRANSACTION_COST_API_URL =
"https://network.radiustech.xyz/api/v1/network/transaction-cost";
/**
* Radius docs currently publish SBC contract address for testnet.
* Set SBC_ASSET to mainnet address in production.
*/
const DEFAULT_SBC_ASSET_TESTNET = "0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb";
const FALLBACK_PERMIT_NAME = "SBC";
const ERC20_PERMIT_ABI = parseAbi([
"function name() view returns (string)",
"function balanceOf(address owner) view returns (uint256)",
"function nonces(address owner) view returns (uint256)",
"function allowance(address owner, address spender) view returns (uint256)",
"function permit(address owner,address spender,uint256 value,uint256 deadline,uint8 v,bytes32 r,bytes32 s)",
"function transferFrom(address from,address to,uint256 value) returns (bool)",
]);
type PaymentConfig = {
paymentAddress: Address;
priceSbc: string;
networkName: string;
networkRpc: string;
chainId: number;
protectedPaths: string;
tokenSymbol: "SBC";
tokenDecimals: number;
asset: Address;
eip712Name: string;
eip712Version: string;
maxTimeoutSeconds: number;
settlementPrivateKey?: `0x${string}`;
transactionCostApiUrl: string;
};
type PaymentRequirement = {
scheme: "exact";
network: string;
maxAmountRequired: string;
resource: string;
description: string;
mimeType: string;
payTo: Address;
maxTimeoutSeconds: number;
asset: Address;
extra: {
name: string;
version: string;
settlementMethod: "permit-transferFrom";
settlementSpender: Address;
};
};
type PermitPaymentPayload = {
x402Version: number;
scheme: "exact";
network: string;
payload: {
kind: "permit-eip2612";
owner: Address;
spender: Address;
value: string;
nonce: string;
deadline: string;
v: number;
r: Hex;
s: Hex;
};
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
const config = buildConfig(env);
if (!config.ok) {
return json({ error: config.error }, 500);
}
// Built-in mobile landing page for seamless MetaMask payment + unlock
if (
path === "/" ||
path === "/pay" ||
path === "/pay/" ||
path === "/landing" ||
path === "/landing/"
) {
return renderLandingPage(request, config.value);
}
const shouldProtect = shouldProtectPath(
path,
config.value.protectedPaths,
);
if (!shouldProtect) {
return serveFromR2(request, env.R2_BUCKET, path);
}
const paymentRequirement = await buildPaymentRequirement(
request,
path,
config.value,
);
const paymentHeader =
request.headers.get("X-Payment") ||
request.headers.get("X-PAYMENT");
if (!paymentHeader) {
return paymentRequiredResponse(paymentRequirement, config.value);
}
// Permit-based verification + settlement flow
const verification = await verifyAndSettlePayment(
paymentHeader,
paymentRequirement,
config.value,
);
if (!verification.ok) {
return json(
{
error: verification.error,
accepts: [paymentRequirement],
x402Version: 1,
details: verification.details,
},
402,
{
"X-Accept-Payment": generatePaymentHeader(config.value),
},
);
}
const contentResponse = await serveFromR2(request, env.R2_BUCKET, path);
const outHeaders = new Headers(contentResponse.headers);
outHeaders.set("X-Payment-Verified", "true");
outHeaders.set("X-Payment-Payer", verification.payer);
outHeaders.set("X-Payment-Transaction", verification.transactionHash);
return new Response(contentResponse.body, {
status: contentResponse.status,
statusText: contentResponse.statusText,
headers: outHeaders,
});
},
};
function buildConfig(
env: Env,
): { ok: true; value: PaymentConfig } | { ok: false; error: string } {
const paymentAddress = normalizeAddress(env.PAYMENT_ADDRESS);
if (!paymentAddress) {
return {
ok: false,
error: "PAYMENT_ADDRESS not configured or invalid",
};
}
const asset = normalizeAddress(env.SBC_ASSET || DEFAULT_SBC_ASSET_TESTNET);
if (!asset) {
return { ok: false, error: "SBC_ASSET not configured or invalid" };
}
const rawDecimals = env.TOKEN_DECIMALS?.trim();
const tokenDecimals =
rawDecimals && Number.isFinite(Number(rawDecimals))
? Number(rawDecimals)
: DEFAULT_SBC_DECIMALS;
const rawChainId = env.NETWORK_CHAIN_ID?.trim();
const chainId =
rawChainId && Number.isFinite(Number(rawChainId))
? Number(rawChainId)
: DEFAULT_RADIUS_MAINNET_CHAIN_ID;
const settlementPrivateKey = normalizeHexKey(env.SETTLEMENT_PRIVATE_KEY);
return {
ok: true,
value: {
paymentAddress,
priceSbc: (
env.PRICE_SBC ||
env.PRICE_USDC ||
DEFAULT_PRICE_SBC
).trim(),
networkName: (env.NETWORK_NAME || DEFAULT_NETWORK_NAME)
.trim()
.toLowerCase(),
networkRpc: (env.NETWORK_RPC || DEFAULT_RADIUS_MAINNET_RPC).trim(),
chainId,
protectedPaths: (env.PROTECTED_PATHS || "/*").trim(),
tokenSymbol: "SBC",
tokenDecimals,
asset,
eip712Name: (env.SBC_EIP712_NAME || "SBC").trim(),
eip712Version: (env.SBC_EIP712_VERSION || "1").trim(),
maxTimeoutSeconds: DEFAULT_MAX_TIMEOUT_SECONDS,
settlementPrivateKey,
transactionCostApiUrl: (
env.TRANSACTION_COST_API_URL || DEFAULT_TRANSACTION_COST_API_URL
).trim(),
},
};
}
async function buildPaymentRequirement(
request: Request,
path: string,
config: PaymentConfig,
): Promise<PaymentRequirement> {
const radiusChain = defineChain({
id: config.chainId,
name: "Radius Network",
network: config.networkName,
nativeCurrency: { name: "RUSD", symbol: "RUSD", decimals: 18 },
rpcUrls: {
default: { http: [config.networkRpc] },
public: { http: [config.networkRpc] },
},
});
const publicClient = createPublicClient({
chain: radiusChain,
transport: http(config.networkRpc),
});
const permitName = await readTokenNameWithRetry(
publicClient,
config.asset,
config.eip712Name,
);
return {
scheme: "exact",
network: config.networkName,
maxAmountRequired: toBaseUnits(config.priceSbc, config.tokenDecimals),
resource: request.url,
description: `Access to ${path}`,
mimeType: "application/octet-stream",
payTo: config.paymentAddress,
maxTimeoutSeconds: config.maxTimeoutSeconds,
asset: config.asset,
extra: {
name: permitName,
version: config.eip712Version,
settlementMethod: "permit-transferFrom",
settlementSpender: config.paymentAddress,
},
};
}
async function verifyAndSettlePayment(
rawHeader: string,
paymentRequirement: PaymentRequirement,
config: PaymentConfig,
): Promise<
| { ok: true; payer: string; transactionHash: string }
| { ok: false; error: string; details?: unknown }
> {
try {
if (!config.settlementPrivateKey) {
return {
ok: false,
error: "SETTLEMENT_PRIVATE_KEY is required for permit-based settlement",
};
}
const radiusChain = defineChain({
id: config.chainId,
name: "Radius Network",
network: config.networkName,
nativeCurrency: { name: "RUSD", symbol: "RUSD", decimals: 18 },
rpcUrls: {
default: { http: [config.networkRpc] },
public: { http: [config.networkRpc] },
},
});
let decoded: PermitPaymentPayload;
try {
decoded = decodePermitPaymentHeader(rawHeader);
} catch (err) {
return {
ok: false,
error: "Malformed X-Payment header",
details: toErrorMessage(err),
};
}
if (decoded.scheme !== "exact") {
return { ok: false, error: "Unsupported payment scheme" };
}
if (decoded.network !== paymentRequirement.network) {
return {
ok: false,
error: "Payment network mismatch",
details: {
received: decoded.network,
expected: paymentRequirement.network,
},
};
}
if (decoded.payload.kind !== "permit-eip2612") {
return { ok: false, error: "Unsupported payment payload kind" };
}
const owner = getAddress(decoded.payload.owner);
const spender = getAddress(decoded.payload.spender);
const signerAccount = privateKeyToAccount(config.settlementPrivateKey);
const settlementSpender = getAddress(signerAccount.address);
if (spender !== settlementSpender) {
return {
ok: false,
error: "Permit spender mismatch",
details: { provided: spender, expected: settlementSpender },
};
}
const value = BigInt(decoded.payload.value);
const required = BigInt(paymentRequirement.maxAmountRequired);
if (value < required) {
return {
ok: false,
error: "Signed permit amount is below required amount",
details: {
signedValue: value.toString(),
requiredValue: required.toString(),
},
};
}
const nowSec = BigInt(Math.floor(Date.now() / 1000));
const deadline = BigInt(decoded.payload.deadline);
if (deadline <= nowSec + 6n) {
return {
ok: false,
error: "Permit deadline expired or near expiry",
details: {
deadline: deadline.toString(),
now: nowSec.toString(),
},
};
}
const publicClient = createPublicClient({
chain: radiusChain,
transport: http(config.networkRpc),
});
// Verify nonce freshness on-chain
const onchainNonce = await readNonceWithRetry(
publicClient,
paymentRequirement.asset,
owner,
);
if (BigInt(decoded.payload.nonce) !== onchainNonce) {
return {
ok: false,
error: "Permit nonce mismatch",
details: {
signedNonce: decoded.payload.nonce,
onchainNonce: onchainNonce.toString(),
},
};
}
// Verify EIP-2612 signature
const recovered = await recoverTypedDataAddress({
domain: {
name: paymentRequirement.extra.name,
version: paymentRequirement.extra.version,
chainId: config.chainId,
verifyingContract: paymentRequirement.asset,
},
types: {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "Permit",
message: {
owner,
spender,
value,
nonce: BigInt(decoded.payload.nonce),
deadline,
},
signature: joinVrs(
decoded.payload.v,
decoded.payload.r,
decoded.payload.s,
),
});
if (getAddress(recovered) !== owner) {
return {
ok: false,
error: "Invalid permit signature",
details: { recovered, expected: owner },
};
}
const balance = await readBalanceWithRetry(
publicClient,
paymentRequirement.asset,
owner,
);
if (balance < required) {
return {
ok: false,
error: "Insufficient SBC balance for payment",
details: {
balance: balance.toString(),
required: required.toString(),
},
};
}
// Settlement (2-step): permit -> transferFrom
const walletClient = createWalletClient({
account: signerAccount,
chain: radiusChain,
transport: http(config.networkRpc),
});
const fixedGasPrice = await getFixedGasPriceWithRetry(
config.transactionCostApiUrl,
config.networkRpc,
);
// Settlement wallet must hold native gas token to submit permit/transfer txs
const settlementNativeBalance = await retryRpc(async () => {
return publicClient.getBalance({
address: signerAccount.address,
});
});
if (settlementNativeBalance <= 0n) {
return {
ok: false,
error: "Settlement wallet has no native gas balance",
details: {
settlementAddress: signerAccount.address,
balance: settlementNativeBalance.toString(),
},
};
}
let permitTx: Hex;
try {
permitTx = await walletClient.writeContract({
address: paymentRequirement.asset,
abi: ERC20_PERMIT_ABI,
functionName: "permit",
args: [
owner,
settlementSpender,
value,
deadline,
decoded.payload.v,
decoded.payload.r,
decoded.payload.s,
],
gasPrice: fixedGasPrice,
type: "legacy",
});
} catch (err) {
return {
ok: false,
error: "Permit transaction failed",
details: toErrorMessage(err),
};
}
const permitReceipt = await publicClient.waitForTransactionReceipt({
hash: permitTx,
});
if (permitReceipt.status !== "success") {
return {
ok: false,
error: "Permit transaction reverted",
details: { txHash: permitTx },
};
}
// Confirm allowance before transferFrom
const allowance = await readAllowanceWithRetry(
publicClient,
paymentRequirement.asset,
owner,
settlementSpender,
);
if (allowance < required) {
return {
ok: false,
error: "Permit did not set sufficient allowance",
details: {
allowance: allowance.toString(),
required: required.toString(),
},
};
}
let transferTx: Hex;
try {
transferTx = await walletClient.writeContract({
address: paymentRequirement.asset,
abi: ERC20_PERMIT_ABI,
functionName: "transferFrom",
args: [owner, paymentRequirement.payTo, required],
gasPrice: fixedGasPrice,
type: "legacy",
});
} catch (err) {
return {
ok: false,
error: "transferFrom settlement transaction failed",
details: toErrorMessage(err),
};
}
const transferReceipt = await publicClient.waitForTransactionReceipt({
hash: transferTx,
});
if (transferReceipt.status !== "success") {
return {
ok: false,
error: "transferFrom settlement transaction reverted",
details: { txHash: transferTx },
};
}
return {
ok: true,
payer: owner,
transactionHash: transferTx,
};
} catch (err) {
return {
ok: false,
error: "Unhandled payment verification error",
details: toErrorMessage(err),
};
}
}
function paymentRequiredResponse(
paymentRequirement: PaymentRequirement,
config: PaymentConfig,
): Response {
return json(
{
error: "Payment required",
accepts: [paymentRequirement],
x402Version: 1,
},
402,
{ "X-Accept-Payment": generatePaymentHeader(config) },
);
}
function shouldProtectPath(path: string, protectedPaths: string): boolean {
const patterns = protectedPaths
.split(",")
.map((p) => p.trim())
.filter(Boolean);
return patterns.some((pattern) => {
if (pattern === "/*") return true;
if (pattern.endsWith("/*")) {
const prefix = pattern.slice(0, -2);
return path.startsWith(prefix);
}
return path === pattern;
});
}
function generatePaymentHeader(config: PaymentConfig): string {
return [
`amount=${config.priceSbc} ${config.tokenSymbol}`,
`address=${config.paymentAddress}`,
`network=${config.networkName}`,
`rpc=${config.networkRpc}`,
`asset=${config.asset}`,
`decimals=${config.tokenDecimals}`,
`settlement=permit-transferFrom`,
].join(", ");
}
/**
* Built-in simple landing page:
* - Connect MetaMask (mobile in-app browser works)
* - Add/switch Radius network
* - Sign EIP-2612 Permit
* - Fetch protected resource with X-Payment
*/
function renderLandingPage(request: Request, config: PaymentConfig): Response {
const origin = new URL(request.url).origin;
const defaultResource = "/songs_of_myself_lines/1.txt";
const amountAtomic = toBaseUnits(config.priceSbc, config.tokenDecimals);
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Songs of Myself — Radius x402 Permit Unlock</title>
<style>
:root{
--bg:#08130c;
--panel:#0f2618;
--panel-2:#143222;
--border:#2e6a43;
--text:#eaf7ee;
--muted:#a9cdb4;
--accent:#4ca86a;
--accent-2:#7bc96f;
--ok:#9be7a8;
--err:#fca5a5;
--paper:#eef7ef;
--ink:#143222;
}
*{box-sizing:border-box}
body{
margin:0;
font-family:-apple-system,system-ui,Segoe UI,Roboto,sans-serif;
color:var(--text);
background:
radial-gradient(1000px 500px at 10% -20%, #1e4d31 0%, transparent 60%),
radial-gradient(900px 450px at 100% 0%, #2f6b3f 0%, transparent 55%),
var(--bg);
min-height:100vh;
}
.wrap{max-width:980px;margin:0 auto;padding:14px}
.hero{
background:linear-gradient(120deg, rgba(76,168,106,0.24), rgba(123,201,111,0.18));
border:1px solid #4f8c63;
border-radius:18px;
padding:14px;
margin-bottom:10px;
}
.hero h1{
margin:0 0 10px;
font-size:32px;
line-height:1.1;
letter-spacing:.2px;
font-family: "Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,serif;
}
.hero p{margin:0;color:var(--muted);max-width:70ch}
.grid{
display:grid;
grid-template-columns:1fr;
gap:14px;
}
@media (min-width:900px){
.grid{grid-template-columns:340px 1fr}
}
.card{
background:linear-gradient(180deg,var(--panel),var(--panel-2));
border:1px solid var(--border);
border-radius:14px;
padding:12px;
}
h2{margin:0 0 8px;font-size:15px;letter-spacing:.2px}
.row{display:grid;gap:8px}
button,input,textarea{
width:100%;
border-radius:10px;
border:1px solid #374781;
background:#0d1530;
color:var(--text);
font-size:14px;
padding:10px 12px;
}
button{
background:linear-gradient(90deg,var(--accent),var(--accent-2));
border:none;
font-weight:700;
cursor:pointer;
}
button.secondary{
background:#1b2756;
border:1px solid #3a4f97;
font-weight:600;
}
button.ghost{
background:transparent;
border:1px dashed #4e62ad;
color:var(--muted);
font-weight:600;
}
textarea{
min-height:160px;
resize:vertical;
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
line-height:1.4;
}
.mono{
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
font-size:12px;
word-break:break-all;
}
.small{font-size:12px}
.muted{color:var(--muted)}
.ok{color:var(--ok)}
.err{color:var(--err)}
.poem{
white-space:normal;
line-height:1.35;
min-height:320px;
padding:20px 18px;
border-radius:12px;
border:1px solid #314178;
background:linear-gradient(180deg,#f2faef,#e6f4e7);
color:var(--ink);
box-shadow: inset 0 1px 0 rgba(255,255,255,.45), 0 12px 30px rgba(9,24,14,.25);
font-family: "Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,serif;
font-size:19px;
letter-spacing:.1px;
text-wrap:pretty;
overflow-x:hidden;
}
.line{
opacity:0;
transform:translateY(6px);
animation:lineReveal .45s ease forwards;
display:block;
width:100%;
line-height:1.35;
font-size:1em;
white-space:normal;
word-break:normal;
overflow-wrap:break-word;
}
@media (max-width: 600px){
.wrap{padding:10px}
.hero{
padding:12px;
margin-bottom:8px;
}
.hero h1{
margin:0 0 6px;
font-size:26px;
}
.hero p{
font-size:13px;
line-height:1.35;
}
.card{
padding:10px;
}
.row{gap:7px}
h2{margin:0 0 6px}
button,input,textarea{
padding:9px 10px;
}
.toolbar{gap:7px}
.poem{
min-height:220px;
font-size:17px;
line-height:1.33;
padding:14px 12px;
}
.line{
font-size:1em;
}
textarea{
min-height:96px;
}
}
.toolbar{
display:grid;
grid-template-columns:1fr 1fr;
gap:8px;
}
.meter{
display:grid;
gap:8px;
margin-top:2px;
}
.meter-top{
display:flex;
justify-content:space-between;
align-items:center;
color:var(--muted);
font-size:12px;
}
.meter-track{
width:100%;
height:10px;
border-radius:999px;
border:1px solid #3f4f8e;
background:#121c40;
overflow:hidden;
}
.meter-fill{
height:100%;
width:0%;
background:linear-gradient(90deg,var(--accent),var(--accent-2));
transition:width .35s ease;
}
@keyframes lineReveal{
to{
opacity:1;
transform:translateY(0);
}
}
details{
border:1px solid #314178;
border-radius:12px;
background:#0d1530;
padding:10px 12px;
}
summary{
cursor:pointer;
color:var(--muted);
font-weight:600;
outline:none;
}
</style>
</head>
<body>
<div class="wrap">
<div class="hero">
<h1>Songs of Myself — Section 51</h1>
<p>This app unlocks only Section 51 of Walt Whitman’s <em>Songs of Myself</em>, one line at a time.</p>
</div>
<div class="grid">
<div class="row">
<div class="card">
<h2>1) Connect wallet</h2>
<div class="row">
<button id="connectBtn">Connect MetaMask</button>
<button id="switchBtn" class="secondary">Switch Network to Radius</button>
<div id="walletInfo" class="mono muted">Not connected</div>
</div>
</div>
<div class="card">
<h2>2) Unlock controls</h2>
<div class="row">
<div class="toolbar">
<button id="payBtn">Sign Permit & Fetch Next Line</button>
<button id="resetBtn" class="ghost">Reset Reading</button>
</div>
<div class="small muted">Current line: <span id="lineNum">1</span></div>
<div class="small muted">Progress: <span id="progressLabel">0 / 17 lines</span></div>
<div id="status" class="small muted">Idle</div>
</div>
</div>
</div>
<div class="card">
<h2>Poem</h2>
<div id="poem" class="poem muted" aria-live="polite">No lines unlocked yet.</div>
<div class="row" style="margin-top:10px">
<textarea id="output" readonly placeholder="Latest fetched line (raw response)"></textarea>
</div>
</div>
<details>
<summary>Network / token config</summary>
<div class="mono" style="margin-top:10px">
network: ${escapeHtml(config.networkName)}<br/>
chainId: ${String(config.chainId)}<br/>
rpc: ${escapeHtml(config.networkRpc)}<br/>
token: SBC (${String(config.tokenDecimals)} decimals)<br/>
asset: ${escapeHtml(config.asset)}<br/>
payTo: ${escapeHtml(config.paymentAddress)}<br/>
settlement spender (server): ${escapeHtml(config.paymentAddress)}<br/>
price: ${escapeHtml(config.priceSbc)} SBC
</div>
</details>
</div>
</div>
<script>
window.__x402Boot = "script-tag-loaded";
window.addEventListener("error", function (e) {
var statusEl = document.getElementById("status");
if (statusEl) {
statusEl.className = "small err";
statusEl.textContent = "Global error: " + ((e && e.message) || "unknown");
}
});
window.addEventListener("unhandledrejection", function (e) {
var statusEl = document.getElementById("status");
if (statusEl) {
statusEl.className = "small err";
statusEl.textContent = "Unhandled rejection: " + String((e && e.reason) || "unknown");
}
});
(function () {
window.__x402Boot = "iife-entered";
var state = {
account: null,
chainIdHex: "0x" + (723).toString(16),
chainIdDec: 723,
networkName: "radius",
rpcUrl: "https://rpc.radiustech.xyz",
payTo: "0x55f210ce4f3a72f7f2ebfce28d0f8c77f4e45e18",
asset: "0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb",
tokenName: "SBC",
tokenVersion: "1",
amountAtomic: "100000000000000000",
timeoutSeconds: 300,
origin: window.location.origin,
poemBasePath: "/songs_of_myself_lines",
nextLine: 1,
unlocked: [],
estimatedTotalLines: 17,
isProcessing: false
};
var el = {
connectBtn: null,
switchBtn: null,
payBtn: null,
resetBtn: null,
walletInfo: null,
status: null,
output: null,
poem: null,
lineNum: null,
progressLabel: null
};
function cacheEls() {
el.connectBtn = document.getElementById("connectBtn");
el.switchBtn = document.getElementById("switchBtn");
el.payBtn = document.getElementById("payBtn");
el.resetBtn = document.getElementById("resetBtn");
el.walletInfo = document.getElementById("walletInfo");
el.status = document.getElementById("status");
el.output = document.getElementById("output");
el.poem = document.getElementById("poem");
el.lineNum = document.getElementById("lineNum");
el.progressLabel = document.getElementById("progressLabel");
}
function setStatus(msg, cls) {
if (!el.status) return;
el.status.className = "small " + (cls || "muted");
el.status.textContent = msg;
}
function safeBootCheck() {
var missing = [];
for (var key in el) {
if (!el[key]) missing.push(key);
}
if (missing.length) {
var raw = document.getElementById("status");
if (raw) {
raw.className = "small err";
raw.textContent = "UI boot error: missing elements (" + missing.join(", ") + ")";
}
return false;
}
return true;
}
function getEthereumProvider() {
if (typeof window === "undefined") return null;
var eth = window.ethereum;
if (!eth) return null;
if (eth.providers && eth.providers.length) {
for (var i = 0; i < eth.providers.length; i++) {
if (eth.providers[i] && eth.providers[i].isMetaMask) return eth.providers[i];
}
return eth.providers[0];
}
return eth;
}
function currentLinePath() {
return state.poemBasePath + "/" + String(state.nextLine) + ".txt";
}
function updateLineIndicator() {
if (el.lineNum) el.lineNum.textContent = String(state.nextLine);
}
function updateProgress() {
if (!el.progressLabel) return;
var unlocked = state.unlocked.length;
el.progressLabel.textContent = unlocked + " / " + state.estimatedTotalLines + " lines";
}
function escapeLineHtml(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function renderPoem() {
if (!el.poem) return;
if (!state.unlocked.length) {
el.poem.textContent = "No lines unlocked yet.";
el.poem.className = "poem muted";
return;
}
el.poem.className = "poem";
el.poem.innerHTML = state.unlocked
.map(function (line) { return '<div class="line">' + escapeLineHtml(line) + "</div>"; })
.join("");
}
function scrollPoemIntoView() {
if (!el.poem) return;
try {
var top = el.poem.getBoundingClientRect().top + window.pageYOffset - 12;
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
} catch (_) {
el.poem.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
function fitPoemLines() {
return;
}
function getErrorMessage(err) {
if (!err) return "Unknown error";
if (typeof err === "string") return err;
if (err.message) return String(err.message);
return String(err);
}
function setPayButtonLoading(loading) {
state.isProcessing = !!loading;
if (!el.payBtn) return;
if (state.isProcessing) {
el.payBtn.disabled = true;
el.payBtn.textContent = "Processing...";
el.payBtn.style.opacity = "0.75";
el.payBtn.style.cursor = "not-allowed";
} else {
el.payBtn.disabled = false;
el.payBtn.textContent = "Sign Permit & Fetch Next Line";
el.payBtn.style.opacity = "";
el.payBtn.style.cursor = "";
}
}
function requestAccounts(provider, onDone) {
provider.request({ method: "eth_requestAccounts" }).then(function (accounts) {
state.account = (accounts && accounts[0]) || null;
if (!state.account) {
setStatus("No account returned from MetaMask.", "err");
return;
}
provider.request({ method: "eth_chainId" }).then(function (chainId) {
if (el.walletInfo) el.walletInfo.textContent = "account=" + state.account + " chainId=" + chainId;
setStatus("Wallet connected", "ok");
if (onDone) onDone();
}).catch(function (e) {
setStatus("Connected, but failed to read chain: " + getErrorMessage(e), "err");
});
}).catch(function (e) {
if (e && e.code === 4001) {
setStatus("Connection request was rejected in MetaMask.", "err");
} else {
setStatus("Failed to connect wallet: " + getErrorMessage(e), "err");
}
});
}
function connect() {
var provider = getEthereumProvider();
if (!provider || typeof provider.request !== "function") {
setStatus("MetaMask not found. Open this page in MetaMask mobile browser.", "err");
return;
}
setStatus("Opening MetaMask connection request...", "muted");
requestAccounts(provider);
}
function switchRadius() {
var provider = getEthereumProvider();
if (!provider || typeof provider.request !== "function") {
setStatus("MetaMask not found.", "err");
return;
}
provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: state.chainIdHex }]
}).then(function () {
setStatus("Switched to Radius network", "ok");
}).catch(function (e) {
if (!(e && e.code === 4902)) {
setStatus("Failed to switch network: " + getErrorMessage(e), "err");
return;
}
provider.request({
method: "wallet_addEthereumChain",
params: [{
chainId: state.chainIdHex,
chainName: "Radius Network",
nativeCurrency: { name: "RUSD", symbol: "RUSD", decimals: 18 },
rpcUrls: [state.rpcUrl],
blockExplorerUrls: ["https://network.radiustech.xyz"]
}]
}).then(function () {
setStatus("Added and switched to Radius network", "ok");
}).catch(function (addErr) {
setStatus("Failed to add Radius network: " + getErrorMessage(addErr), "err");
});
});
}
function buildPermitPayload(resourceUrl, onSuccess, onError) {
var provider = getEthereumProvider();
if (!provider || typeof provider.request !== "function") {
onError(new Error("MetaMask provider unavailable"));
return;
}
fetch(resourceUrl, { method: "GET" })
.then(function (challenge) { return challenge.json(); })
.then(function (challengeJson) {
var requirement = challengeJson && challengeJson.accepts && challengeJson.accepts[0];
if (!requirement) throw new Error("No payment requirement from server");
var maxAmountRequired = requirement.maxAmountRequired;
var now = Math.floor(Date.now() / 1000);
var deadline = (BigInt(now) + BigInt(state.timeoutSeconds)).toString();
var settlementSpender = (requirement.extra && requirement.extra.settlementSpender) || requirement.payTo;
var spender = settlementSpender;
var nonceCallData = "0x7ecebe00000000000000000000000000" + state.account.slice(2).toLowerCase();
var nonceAsset = requirement.asset || state.asset;
function readPermitNonceWithPending() {
return provider.request({
method: "eth_call",
params: [{ to: nonceAsset, data: nonceCallData }, "pending"]
}).catch(function () {
return provider.request({
method: "eth_call",
params: [{ to: nonceAsset, data: nonceCallData }, "latest"]
});
});
}
return readPermitNonceWithPending().then(function (nonceHex) {
var nonce = BigInt(nonceHex).toString();
var domain = {
name: (requirement.extra && requirement.extra.name) || state.tokenName,
version: (requirement.extra && requirement.extra.version) || state.tokenVersion,
chainId: state.chainIdDec,
verifyingContract: requirement.asset || state.asset
};
var message = {
owner: state.account,
spender: spender,
value: maxAmountRequired,
nonce: nonce,
deadline: deadline
};
setStatus("Requesting EIP-2612 permit signature in MetaMask...", "muted");
return provider.request({
method: "eth_signTypedData_v4",
params: [
state.account,
JSON.stringify({
domain: domain,
message: message,
primaryType: "Permit",
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
],
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
}
})
]
}).then(function (sig) {
var sigNo0x = sig.slice(2);
var r = "0x" + sigNo0x.slice(0, 64);
var s = "0x" + sigNo0x.slice(64, 128);
var v = parseInt(sigNo0x.slice(128, 130), 16);
if (v < 27) v += 27;
var paymentHeaderObj = {
x402Version: 1,
scheme: "exact",
network: requirement.network || state.networkName,
payload: {
kind: "permit-eip2612",
owner: state.account,
spender: spender,
value: maxAmountRequired,
nonce: nonce,
deadline: deadline,
v: v,
r: r,
s: s
}
};
onSuccess(btoa(unescape(encodeURIComponent(JSON.stringify(paymentHeaderObj)))));
});
});
})
.catch(onError);
}
function payAndFetchNextLine(retryCount) {
var currentRetry = Number(retryCount || 0);
var maxNonceRetries = 3;
if (state.isProcessing) {
setStatus("Please wait, previous unlock is still processing...", "muted");
return;
}
var provider = getEthereumProvider();
if (!provider) {
setStatus("MetaMask not found.", "err");
return;
}
setPayButtonLoading(true);
if (currentRetry > 0) {
setStatus("Retrying unlock with fresh nonce (" + currentRetry + "/" + maxNonceRetries + ")...", "muted");
}
var continueFlow = function () {
var path = currentLinePath();
var resourceUrl = state.origin + path;
buildPermitPayload(resourceUrl, function (header) {
setStatus("Submitting paid request for line " + state.nextLine + "...", "muted");
fetch(resourceUrl, {
method: "GET",
headers: { "X-Payment": header }
}).then(function (res) {
return res.text().then(function (text) {
if (el.output) el.output.value = text;
if (!res.ok) {
if (res.status === 404) {
setStatus("Reached end of poem at line " + state.nextLine + ".", "ok");
return;
}
if (res.status === 402) {
var details = "";
var shouldRetryNonce = false;
try {
var parsed = JSON.parse(text);
if (parsed && parsed.error) {
details = String(parsed.error);
if (parsed.details && parsed.details.signedNonce && parsed.details.onchainNonce) {
details += " (signedNonce=" + parsed.details.signedNonce + ", onchainNonce=" + parsed.details.onchainNonce + ")";
}
shouldRetryNonce = String(parsed.error).toLowerCase().indexOf("permit nonce mismatch") !== -1;
}
} catch (_) {}
if (shouldRetryNonce && currentRetry < maxNonceRetries) {
setStatus("Permit nonce advanced on-chain. Retrying with fresh nonce...", "muted");
setPayButtonLoading(false);
payAndFetchNextLine(currentRetry + 1);
return;
}
setStatus("Payment required (402)" + (details ? ": " + details : ""), "err");
return;
}
setStatus("Request failed: HTTP " + res.status, "err");
return;
}
state.unlocked.push(text.replace(/\s+$/, ""));
state.nextLine += 1;
updateLineIndicator();
updateProgress();
renderPoem();
scrollPoemIntoView();
setStatus("Unlocked line " + (state.nextLine - 1) + ".", "ok");
});
}).catch(function (e) {
setStatus("Error: " + getErrorMessage(e), "err");
}).finally(function () {
setPayButtonLoading(false);
});
}, function (e) {
var msg = getErrorMessage(e);
if (msg && msg.toLowerCase().indexOf("nonce mismatch") !== -1 && currentRetry < maxNonceRetries) {
setStatus("Nonce changed before submit. Retrying with fresh nonce...", "muted");
setPayButtonLoading(false);
payAndFetchNextLine(currentRetry + 1);
return;
}
setStatus("Error: " + msg, "err");
setPayButtonLoading(false);
});
};
if (!state.account) {
setStatus("Connecting wallet first...", "muted");
requestAccounts(provider, function () {
if (!state.account) {
setPayButtonLoading(false);
return;
}
continueFlow();
});
return;
}
continueFlow();
}
function resetReading() {
if (state.isProcessing) {
setStatus("Cannot reset while a payment request is in progress.", "err");
return;
}
state.nextLine = 1;
state.unlocked = [];
if (el.output) el.output.value = "";
updateLineIndicator();
updateProgress();
renderPoem();
setStatus("Reading reset to line 1.", "muted");
}
function wireEvents() {
el.connectBtn.onclick = connect;
el.switchBtn.onclick = switchRadius;
el.payBtn.onclick = payAndFetchNextLine;
el.resetBtn.onclick = resetReading;
var provider = getEthereumProvider();
if (provider && typeof provider.on === "function") {
provider.on("accountsChanged", function (accounts) {
state.account = (accounts && accounts[0]) || null;
if (el.walletInfo) {
el.walletInfo.textContent = state.account ? ("account=" + state.account) : "Not connected";
}
});
provider.on("chainChanged", function (chainId) {
if (el.walletInfo) {
el.walletInfo.textContent =
(state.account ? ("account=" + state.account + " ") : "") + "chainId=" + chainId;
}
});
}
}
function init() {
window.__x402Boot = "init-start";
cacheEls();
window.__x402Boot = "elements-cached";
if (!safeBootCheck()) {
window.__x402Boot = "boot-check-failed";
return;
}
wireEvents();
window.__x402Boot = "events-wired";
updateLineIndicator();
updateProgress();
renderPoem();
setStatus("Ready. Connect MetaMask to begin.", "muted");
fitPoemLines();
if (typeof window !== "undefined") {
window.addEventListener("resize", fitPoemLines);
}
window.__x402Boot = "ready";
}
if (document.readyState === "loading") {
window.__x402Boot = "waiting-domcontentloaded";
document.addEventListener("DOMContentLoaded", function () {
window.__x402Boot = "domcontentloaded-fired";
init();
});
} else {
window.__x402Boot = "document-already-ready";
init();
}
})();
</script>
</body>
</html>`;
return new Response(html, {
status: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
function decodePermitPaymentHeader(rawHeader: string): PermitPaymentPayload {
const decoded = safeBase64Decode(rawHeader);
const obj = JSON.parse(decoded) as PermitPaymentPayload;
if (
!obj ||
typeof obj !== "object" ||
obj.scheme !== "exact" ||
!obj.payload ||
obj.payload.kind !== "permit-eip2612"
) {
throw new Error("Invalid permit payment payload");
}
obj.payload.owner = getAddress(obj.payload.owner);
obj.payload.spender = getAddress(obj.payload.spender);
if (!isHex(obj.payload.r) || !isHex(obj.payload.s)) {
throw new Error("Invalid permit signature parts");
}
return obj;
}
function joinVrs(v: number, r: Hex, s: Hex): Hex {
const vv = v.toString(16).padStart(2, "0");
return `0x${r.slice(2)}${s.slice(2)}${vv}` as Hex;
}
function safeBase64Decode(data: string): string {
if (
typeof globalThis !== "undefined" &&
typeof globalThis.atob === "function"
) {
return globalThis.atob(data);
}
// @ts-ignore
return Buffer.from(data, "base64").toString("utf-8");
}
async function readTokenNameWithRetry(
publicClient: ReturnType<typeof createPublicClient>,
token: Address,
fallbackName: string,
): Promise<string> {
try {
return await retryRpc(async () => {
const value = (await publicClient.readContract({
address: token,
abi: ERC20_PERMIT_ABI,
functionName: "name",
args: [],
})) as string;
return value;
});
} catch {
return fallbackName || FALLBACK_PERMIT_NAME;
}
}
async function readNonceWithRetry(
publicClient: ReturnType<typeof createPublicClient>,
token: Address,
owner: Address,
): Promise<bigint> {
return retryRpc(async () => {
const value = (await publicClient.readContract({
address: token,
abi: ERC20_PERMIT_ABI,
functionName: "nonces",
args: [owner],
})) as bigint;
return value;
});
}
async function readAllowanceWithRetry(
publicClient: ReturnType<typeof createPublicClient>,
token: Address,
owner: Address,
spender: Address,
): Promise<bigint> {
return retryRpc(async () => {
const value = (await publicClient.readContract({
address: token,
abi: ERC20_PERMIT_ABI,
functionName: "allowance",
args: [owner, spender],
})) as bigint;
return value;
});
}
async function readBalanceWithRetry(
publicClient: ReturnType<typeof createPublicClient>,
token: Address,
owner: Address,
): Promise<bigint> {
return retryRpc(async () => {
const value = (await publicClient.readContract({
address: token,
abi: ERC20_PERMIT_ABI,
functionName: "balanceOf",
args: [owner],
})) as bigint;
return value;
});
}
async function getFixedGasPriceWithRetry(
_transactionCostApiUrl: string,
_networkRpc: string,
): Promise<bigint> {
// Stubbed fixed transaction-cost response (auth-gated API workaround):
// {
// "cost_usd": 0.000092887004460096,
// "gas_price_wei": "0x3ac525e0",
// "gas_used": 94206,
// "last_updated": 1772309045680
// }
return hexToBigInt("0x3ac525e0");
}
async function retryRpc<T>(fn: () => Promise<T>): Promise<T> {
let lastErr: unknown;
for (let attempt = 1; attempt <= RPC_RETRY_MAX_ATTEMPTS; attempt++) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (!isRateLimitError(err) || attempt === RPC_RETRY_MAX_ATTEMPTS)
throw err;
await sleep(RPC_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
}
}
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}
function isRateLimitError(err: unknown): boolean {
const msg = toErrorMessage(err).toLowerCase();
return (
msg.includes("429") ||
msg.includes("too many") ||
msg.includes("rate limit") ||
msg.includes("too many connections")
);
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function toBaseUnits(amount: string, decimals: number): string {
const normalized = amount.trim();
if (!normalized) return "0";
const negative = normalized.startsWith("-");
const unsigned = negative ? normalized.slice(1) : normalized;
const [wholeRaw, fracRaw = ""] = unsigned.split(".");
const whole = wholeRaw.replace(/\D/g, "") || "0";
const frac = fracRaw.replace(/\D/g, "");
const wholeUnits = BigInt(whole) * 10n ** BigInt(decimals);
const fracPadded = frac.slice(0, decimals).padEnd(decimals, "0");
const fracUnits = fracPadded ? BigInt(fracPadded) : 0n;
const value = wholeUnits + fracUnits;
return negative ? `-${value.toString()}` : value.toString();
}
function normalizeAddress(value?: string): Address | null {
if (!value) return null;
try {
return getAddress(value.trim());
} catch {
return null;
}
}
function normalizeHexKey(value?: string): `0x${string}` | undefined {
if (!value) return undefined;
const v = value.trim();
if (!v.startsWith("0x")) return undefined;
if (v.length !== 66) return undefined;
return v as `0x${string}`;
}
function toErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}
function json(
body: unknown,
status = 200,
extraHeaders?: Record<string, string>,
): Response {
const headers = new Headers({ "Content-Type": "application/json" });
if (extraHeaders) {
for (const [k, v] of Object.entries(extraHeaders)) headers.set(k, v);
}
return new Response(JSON.stringify(body), { status, headers });
}
function escapeHtml(str: string): string {
return str
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
async function serveFromR2(
request: Request,
bucket: R2Bucket,
path: string,
): Promise<Response> {
const objectKey = path.startsWith("/") ? path.slice(1) : path;
if (objectKey === "") {
return new Response("Index listing not supported", { status: 404 });
}
const candidateKeys = [objectKey];
if (objectKey.startsWith("songs_of_myself/")) {
candidateKeys.push(
objectKey.replace(/^songs_of_myself\//, "songs_of_myself_lines/"),
);
}
let object: R2ObjectBody | null = null;
for (const key of candidateKeys) {
object = await bucket.get(key);
if (object) break;
}
if (!object) {
return new Response("File not found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
if (request.method === "HEAD") {
return new Response(null, { headers });
}
return new Response(object.body, { headers });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment