diff --git a/.github/workflows/check_paywall_template.yml b/.github/workflows/check_paywall_template.yml
new file mode 100644
index 0000000000..2ec793ece9
--- /dev/null
+++ b/.github/workflows/check_paywall_template.yml
@@ -0,0 +1,90 @@
+name: Check Paywall Template
+
+on:
+ pull_request:
+ paths:
+ - "typescript/packages/http/paywall/**"
+ - "typescript/packages/mechanisms/evm/**"
+ - "typescript/pnpm-lock.yaml"
+ - "python/x402/http/paywall/**"
+ - "go/http/evm_paywall_template.go"
+ - "go/http/svm_paywall_template.go"
+ - "go/http/avm_paywall_template.go"
+ - ".github/workflows/check_paywall_template.yml"
+
+jobs:
+ check-paywall-template:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
+ with:
+ version: 10.7.0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+ cache: "pnpm"
+ cache-dependency-path: ./typescript
+
+ - name: Install dependencies
+ working-directory: ./typescript
+ run: pnpm install --frozen-lockfile
+
+ - name: Build workspace dependencies
+ working-directory: ./typescript
+ run: pnpm -r run build
+
+ - name: Regenerate paywall bundles
+ working-directory: ./typescript/packages/http/paywall
+ run: pnpm run build:paywall
+
+ - name: Verify regenerated templates match committed files
+ run: |
+ git diff --exit-code -- \
+ typescript/packages/http/paywall/src/evm/gen/template.ts \
+ typescript/packages/http/paywall/src/svm/gen/template.ts \
+ typescript/packages/http/paywall/src/avm/gen/template.ts \
+ python/x402/http/paywall/evm_paywall_template.py \
+ python/x402/http/paywall/svm_paywall_template.py \
+ python/x402/http/paywall/avm_paywall_template.py \
+ go/http/evm_paywall_template.go \
+ go/http/svm_paywall_template.go \
+ go/http/avm_paywall_template.go \
+ || {
+ echo ""
+ echo "::error::Paywall bundled templates are stale. Run 'pnpm --filter @x402/paywall run build:paywall' locally and commit the result."
+ exit 1
+ }
+
+ - name: Verify determinism (re-run build, compare to first run)
+ working-directory: ./typescript/packages/http/paywall
+ run: |
+ first_sha=$(sha256sum \
+ src/evm/gen/template.ts \
+ src/svm/gen/template.ts \
+ src/avm/gen/template.ts \
+ ../../../../python/x402/http/paywall/evm_paywall_template.py \
+ ../../../../python/x402/http/paywall/svm_paywall_template.py \
+ ../../../../python/x402/http/paywall/avm_paywall_template.py \
+ ../../../../go/http/evm_paywall_template.go \
+ ../../../../go/http/svm_paywall_template.go \
+ ../../../../go/http/avm_paywall_template.go)
+ pnpm run build:paywall
+ second_sha=$(sha256sum \
+ src/evm/gen/template.ts \
+ src/svm/gen/template.ts \
+ src/avm/gen/template.ts \
+ ../../../../python/x402/http/paywall/evm_paywall_template.py \
+ ../../../../python/x402/http/paywall/svm_paywall_template.py \
+ ../../../../python/x402/http/paywall/avm_paywall_template.py \
+ ../../../../go/http/evm_paywall_template.go \
+ ../../../../go/http/svm_paywall_template.go \
+ ../../../../go/http/avm_paywall_template.go)
+ if [ "$first_sha" != "$second_sha" ]; then
+ echo "::error::build:paywall is non-deterministic across repeat runs. See x4-23b for context."
+ diff <(echo "$first_sha") <(echo "$second_sha")
+ exit 1
+ fi
diff --git a/go/http/avm_paywall_template.go b/go/http/avm_paywall_template.go
new file mode 100644
index 0000000000..dfb6ecd05b
--- /dev/null
+++ b/go/http/avm_paywall_template.go
@@ -0,0 +1,5 @@
+// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
+package http
+
+// AVMPaywallTemplate is the pre-built AVM paywall template with inlined CSS and JS
+const AVMPaywallTemplate = "
\n \n \n Payment Required\n \n \n \n \n "
diff --git a/go/http/evm_paywall_template.go b/go/http/evm_paywall_template.go
index 83867a322e..a9d8e98441 100644
--- a/go/http/evm_paywall_template.go
+++ b/go/http/evm_paywall_template.go
@@ -2,4 +2,4 @@
package http
// EVMPaywallTemplate is the pre-built EVM paywall template with inlined CSS and JS
-const EVMPaywallTemplate = "\n \n \n Payment Required\n \n \n \n \n "
+const EVMPaywallTemplate = "\n \n \n Payment Required\n \n \n \n \n "
diff --git a/go/http/svm_paywall_template.go b/go/http/svm_paywall_template.go
index dd586b94ee..a5aa40b99c 100644
--- a/go/http/svm_paywall_template.go
+++ b/go/http/svm_paywall_template.go
@@ -2,4 +2,4 @@
package http
// SVMPaywallTemplate is the pre-built SVM paywall template with inlined CSS and JS
-const SVMPaywallTemplate = "\n \n \n Payment Required\n \n \n \n \n "
+const SVMPaywallTemplate = "\n \n \n Payment Required\n \n \n \n \n "
diff --git a/python/x402/http/paywall/avm_paywall_template.py b/python/x402/http/paywall/avm_paywall_template.py
new file mode 100644
index 0000000000..53cc1c7758
--- /dev/null
+++ b/python/x402/http/paywall/avm_paywall_template.py
@@ -0,0 +1,2 @@
+# THIS FILE IS AUTO-GENERATED - DO NOT EDIT
+AVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n \n \n \n '
diff --git a/python/x402/http/paywall/evm_paywall_template.py b/python/x402/http/paywall/evm_paywall_template.py
index d39b605ad5..5287909f68 100644
--- a/python/x402/http/paywall/evm_paywall_template.py
+++ b/python/x402/http/paywall/evm_paywall_template.py
@@ -1,2 +1,2 @@
# THIS FILE IS AUTO-GENERATED - DO NOT EDIT
-EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n \n \n \n '
+EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n \n \n \n '
diff --git a/python/x402/http/paywall/svm_paywall_template.py b/python/x402/http/paywall/svm_paywall_template.py
index f1d95458ad..400883705f 100644
--- a/python/x402/http/paywall/svm_paywall_template.py
+++ b/python/x402/http/paywall/svm_paywall_template.py
@@ -1,2 +1,2 @@
# THIS FILE IS AUTO-GENERATED - DO NOT EDIT
-SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n \n \n \n '
+SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n \n \n \n '
diff --git a/typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md b/typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md
new file mode 100644
index 0000000000..65dcceb8f3
--- /dev/null
+++ b/typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md
@@ -0,0 +1,22 @@
+---
+"@x402/paywall": patch
+---
+
+chore(paywall): regenerate EVM/SVM/AVM bundles for viem 2.47.12
+
+The bundled paywall templates were last regenerated against a viem version
+that predates chain definitions for Mezo (`eip155:31612`), Mezo Testnet
+(`eip155:31611`), MegaETH (`eip155:4326`), MegaETH Testnet (`eip155:6343`),
+Stable (`eip155:988`), Stable Testnet (`eip155:2201`), Radius
+(`eip155:723487`), Radius Testnet (`eip155:72344`), and 33 other chains. The
+lockfile moved to viem 2.47.12 in PR #2013 but the bundle was not
+regenerated, so @x402/paywall hard-threw `Unsupported chain ID` at component
+init for payments on those chains.
+
+This commit regenerates all nine generated files (TypeScript, Python, and
+Go templates for EVM/SVM/AVM) against the current lockfile. Total unique
+chain IDs in the EVM bundle goes from 635 to 676.
+
+No source code changes. Paired with a new PR-time drift check
+(`.github/workflows/check_paywall_template.yml`) so this stays fresh
+across future viem bumps.
diff --git a/typescript/packages/http/paywall/src/avm/build.ts b/typescript/packages/http/paywall/src/avm/build.ts
index 329cff2325..8dcbd3538c 100644
--- a/typescript/packages/http/paywall/src/avm/build.ts
+++ b/typescript/packages/http/paywall/src/avm/build.ts
@@ -3,6 +3,7 @@ import { htmlPlugin } from "@craftamap/esbuild-plugin-html";
import fs from "fs";
import path from "path";
import { getBaseTemplate } from "../baseTemplate";
+import { formatTypeScript, toPythonStringLiteral } from "../genHelpers";
// AVM-specific build - only bundles Algorand dependencies
const DIST_DIR = "src/avm/dist";
@@ -84,16 +85,17 @@ async function build() {
if (fs.existsSync(OUTPUT_HTML)) {
const html = fs.readFileSync(OUTPUT_HTML, "utf8");
- const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
+ const rawTsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
/**
* The pre-built AVM paywall template with inlined CSS and JS
*/
export const AVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)};
`;
+ const tsContent = await formatTypeScript(OUTPUT_TS, rawTsContent);
// Generate Python template file
const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT
-AVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}
+AVM_PAYWALL_TEMPLATE = ${toPythonStringLiteral(html)}
`;
// Generate Go template file
diff --git a/typescript/packages/http/paywall/src/avm/gen/template.ts b/typescript/packages/http/paywall/src/avm/gen/template.ts
index cd7ad5faac..c2d4680985 100644
--- a/typescript/packages/http/paywall/src/avm/gen/template.ts
+++ b/typescript/packages/http/paywall/src/avm/gen/template.ts
@@ -3,4 +3,4 @@
* The pre-built AVM paywall template with inlined CSS and JS
*/
export const AVM_PAYWALL_TEMPLATE =
- '\n \n \n Payment Required\n \n \n \n \n ';
+ '\n \n \n Payment Required\n \n \n \n \n ';
diff --git a/typescript/packages/http/paywall/src/evm/build.ts b/typescript/packages/http/paywall/src/evm/build.ts
index 0d7d924882..af1a093b2d 100644
--- a/typescript/packages/http/paywall/src/evm/build.ts
+++ b/typescript/packages/http/paywall/src/evm/build.ts
@@ -3,6 +3,7 @@ import { htmlPlugin } from "@craftamap/esbuild-plugin-html";
import fs from "fs";
import path from "path";
import { getBaseTemplate } from "../baseTemplate";
+import { formatTypeScript, toPythonStringLiteral } from "../genHelpers";
// EVM-specific build - only bundles EVM dependencies
const DIST_DIR = "src/evm/dist";
@@ -76,16 +77,17 @@ async function build() {
if (fs.existsSync(OUTPUT_HTML)) {
const html = fs.readFileSync(OUTPUT_HTML, "utf8");
- const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
+ const rawTsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
/**
* The pre-built EVM paywall template with inlined CSS and JS
*/
export const EVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)};
`;
+ const tsContent = await formatTypeScript(OUTPUT_TS, rawTsContent);
// Generate Python template file
const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT
-EVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}
+EVM_PAYWALL_TEMPLATE = ${toPythonStringLiteral(html)}
`;
// Generate Go template file
diff --git a/typescript/packages/http/paywall/src/evm/gen/template.ts b/typescript/packages/http/paywall/src/evm/gen/template.ts
index 73ce58d52c..c2c2a5836a 100644
--- a/typescript/packages/http/paywall/src/evm/gen/template.ts
+++ b/typescript/packages/http/paywall/src/evm/gen/template.ts
@@ -3,4 +3,4 @@
* The pre-built EVM paywall template with inlined CSS and JS
*/
export const EVM_PAYWALL_TEMPLATE =
- '\n \n \n Payment Required\n \n \n \n \n ';
+ '\n \n \n Payment Required\n \n \n \n \n ';
diff --git a/typescript/packages/http/paywall/src/genHelpers.ts b/typescript/packages/http/paywall/src/genHelpers.ts
new file mode 100644
index 0000000000..0f076291d2
--- /dev/null
+++ b/typescript/packages/http/paywall/src/genHelpers.ts
@@ -0,0 +1,45 @@
+import prettier from "prettier";
+
+/**
+ * Format a TypeScript file's contents using the paywall package's prettier
+ * config. Used to keep auto-generated template files byte-identical to what
+ * `pnpm run format` would emit, so CI drift checks stay reliable.
+ *
+ * @param filePath - Absolute or relative path used by prettier for config
+ * resolution and filetype inference.
+ * @param contents - Source contents to format.
+ * @returns The prettier-formatted contents.
+ */
+export async function formatTypeScript(filePath: string, contents: string): Promise {
+ const config = await prettier.resolveConfig(filePath);
+ return prettier.format(contents, { ...config, filepath: filePath });
+}
+
+/**
+ * Serialize a string as a Python source literal, choosing the quote style
+ * that requires fewer escapes. Mirrors ruff/black's default behavior so that
+ * `ruff format --check` is a no-op on generated files.
+ *
+ * @param s - Input string to serialize.
+ * @returns A Python string literal (quoted and escaped) representing `s`.
+ */
+export function toPythonStringLiteral(s: string): string {
+ const singleCount = (s.match(/'/g) || []).length;
+ const doubleCount = (s.match(/"/g) || []).length;
+ const useSingle = doubleCount > singleCount;
+ const quote = useSingle ? "'" : '"';
+
+ let body = s
+ .replace(/\\/g, "\\\\")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/\t/g, "\\t")
+ .replace(/\f/g, "\\f")
+ .replace(/\v/g, "\\v")
+ .replace(
+ /[\x00-\x08\x0e-\x1f\x7f]/g,
+ c => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`,
+ );
+ body = useSingle ? body.replace(/'/g, "\\'") : body.replace(/"/g, '\\"');
+ return `${quote}${body}${quote}`;
+}
diff --git a/typescript/packages/http/paywall/src/svm/build.ts b/typescript/packages/http/paywall/src/svm/build.ts
index bf1bfe0de9..d0101004e9 100644
--- a/typescript/packages/http/paywall/src/svm/build.ts
+++ b/typescript/packages/http/paywall/src/svm/build.ts
@@ -3,6 +3,7 @@ import { htmlPlugin } from "@craftamap/esbuild-plugin-html";
import fs from "fs";
import path from "path";
import { getBaseTemplate } from "../baseTemplate";
+import { formatTypeScript, toPythonStringLiteral } from "../genHelpers";
// SVM-specific build - only bundles Solana dependencies
const DIST_DIR = "src/svm/dist";
@@ -76,16 +77,17 @@ async function build() {
if (fs.existsSync(OUTPUT_HTML)) {
const html = fs.readFileSync(OUTPUT_HTML, "utf8");
- const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
+ const rawTsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT
/**
* The pre-built SVM paywall template with inlined CSS and JS
*/
export const SVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)};
`;
+ const tsContent = await formatTypeScript(OUTPUT_TS, rawTsContent);
// Generate Python template file
const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT
-SVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}
+SVM_PAYWALL_TEMPLATE = ${toPythonStringLiteral(html)}
`;
// Generate Go template file
diff --git a/typescript/packages/http/paywall/src/svm/gen/template.ts b/typescript/packages/http/paywall/src/svm/gen/template.ts
index a6e7bd39b0..ef3f3fcd18 100644
--- a/typescript/packages/http/paywall/src/svm/gen/template.ts
+++ b/typescript/packages/http/paywall/src/svm/gen/template.ts
@@ -3,4 +3,4 @@
* The pre-built SVM paywall template with inlined CSS and JS
*/
export const SVM_PAYWALL_TEMPLATE =
- '\n \n \n Payment Required\n \n \n \n \n ';
+ '\n \n \n Payment Required\n \n \n \n \n ';