Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save wenakita/e530a91d53f5296f1447cfb728cd69fd to your computer and use it in GitHub Desktop.
$feedback — leave feedback for agent 2205. In-iframe ERC-8004 Reputation Registry terminal on Zora. Sign once, writes to 0x8004baa1…9b63 on Base.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>leave feedback for agent 2205</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;
--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, 560px); 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.1; 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: 46ch; }
.sub a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--dim); text-underline-offset: 3px; }
.sub a:hover { text-decoration-color: var(--accent); }
.segment { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.seg-btn {
padding: 12px 10px; border: 1px solid #1a1a1a; background: #070707;
color: var(--muted); cursor: pointer; border-radius: 2px;
font-family: inherit; font-size: 12px; letter-spacing: 0.04em;
text-transform: uppercase; transition: all 0.15s ease;
}
.seg-btn:hover { border-color: var(--dim); color: var(--fg); }
.seg-btn.active[data-value="positive"] { border-color: var(--pos); color: var(--pos); background: #0a1a10; }
.seg-btn.active[data-value="neutral"] { border-color: var(--muted); color: var(--fg); background: #121212; }
.seg-btn.active[data-value="critical"] { border-color: var(--neg); color: var(--neg); background: #1a0a0a; }
.field { display: flex; flex-direction: column; gap: 6px; }
.label { color: var(--muted); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; }
select, textarea {
background: #070707; border: 1px solid #1a1a1a; color: var(--fg);
font-family: inherit; font-size: 12px; padding: 10px 12px;
border-radius: 2px; resize: vertical;
}
textarea { min-height: 56px; line-height: 1.6; }
select:focus, textarea:focus { outline: 0; border-color: var(--accent); }
.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; }
.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; }
.tx-panel, .diag-panel {
border: 1px solid #1a1a1a; background: #070707; padding: 12px 14px;
border-radius: 2px; font-size: 11px;
}
.tx-panel a, .diag-panel a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--dim); text-underline-offset: 3px; }
.tx-panel a:hover, .diag-panel a:hover { text-decoration-color: var(--accent); }
.tx-panel .row, .diag-panel .row { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; }
.tx-panel .k, .diag-panel .k { color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; font-size: 10px; }
.tx-panel .v, .diag-panel .v { color: var(--fg); font-family: "JetBrains Mono", monospace; text-align: right; word-break: break-all; }
/* Wallet picker modal */
.picker-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
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, 400px);
display: flex; flex-direction: column; gap: 8px;
}
.picker .h { font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.picker button {
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;
}
.picker button:hover { background: #242424; border-color: var(--accent); }
.picker button img { width: 24px; height: 24px; border-radius: 4px; }
.picker .cancel { text-align: center; background: transparent; border: 0; color: var(--muted); padding: 8px; margin-top: 4px; }
.picker .cancel:hover { color: var(--fg); }
.footnote { color: var(--dim); font-size: 10px; letter-spacing: 0.04em; line-height: 1.6; }
.footnote code { color: var(--muted); }
.hidden { display: none !important; }
/* Collapsed diagnostic summary */
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 { display: none; }
details.diag summary::-webkit-details-marker { display: none; }
details.diag summary:hover { color: var(--fg); }
details.diag[open] summary { margin-bottom: 6px; }
</style>
</head>
<body>
<div class="card">
<div class="kicker">ERC-8004 · BASE · AGENT 2205</div>
<h1 class="headline">leave <em>feedback</em>.</h1>
<p class="sub">
A public feedback submission to agent 2205, written to the
<a href="https://basescan.org/address/0x8004baa17c55a88189ae136b182e5fda19de9b63" target="_blank" rel="noopener">Reputation Registry</a>. One signature, permanent on-chain, readable by the agent.
</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>
<div class="field">
<div class="label">signal</div>
<div class="segment" id="segment">
<button class="seg-btn" data-value="positive" type="button">positive</button>
<button class="seg-btn active" data-value="neutral" type="button">neutral</button>
<button class="seg-btn" data-value="critical" type="button">critical</button>
</div>
</div>
<div class="field">
<div class="label">category</div>
<select id="tag">
<option value="overall">overall</option>
<option value="responsiveness">responsiveness</option>
<option value="accuracy">accuracy</option>
<option value="reliability">reliability</option>
<option value="ux">ux</option>
<option value="pricing">pricing</option>
</select>
</div>
<div class="field">
<div class="label">message (optional — written as feedbackURI on chain)</div>
<textarea id="message" maxlength="800" placeholder="what should agent 2205 hear?"></textarea>
</div>
<div class="actions">
<button class="primary" id="submit" type="button">connect wallet</button>
</div>
<!-- Tx panel (hidden until there's something to show) -->
<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 class="row"><div class="k">registry</div>
<div class="v"><a href="https://basescan.org/address/0x8004baa17c55a88189ae136b182e5fda19de9b63" target="_blank" rel="noopener">0x8004baa1…9b63</a></div></div>
</div>
<!-- Diagnostics (collapsed by default; expand if something's off). -->
<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">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">
calls <code>giveFeedback(2205, …)</code> on the ERC-8004 Reputation Registry. Self-feedback is blocked by the contract — if you own agent 2205, use a different wallet.
</div>
</div>
<!-- Wallet picker modal -->
<div class="picker-overlay" id="picker-overlay">
<div class="picker">
<div class="h">choose a wallet</div>
<div id="picker-list"></div>
<button class="cancel" id="picker-cancel" type="button">cancel</button>
</div>
</div>
<script>
// ─── Constants ───────────────────────────────────────────────────────
const AGENT_ID = 2205n;
const REG_ADDR = "0x8004baa17c55a88189ae136b182e5fda19de9b63";
const BASE_CHAIN_ID = 8453;
const BASE_CHAIN_HEX = "0x2105";
// giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)
const SELECTOR = "3c036a7e";
// ─── State ───────────────────────────────────────────────────────────
// `providers` is the EIP-6963 discovered wallet set. `activeProvider`
// is whichever provider the user picked (or the sole one) and is what
// we actually call .request() on. `window.ethereum` is only a fallback.
let state = {
sentiment: "neutral",
address: null,
chainId: null,
txHash: null,
txStatus: null,
txError: null,
providers: [], // [{info: {name, icon, rdns, uuid}, provider}]
activeProvider: null, // a provider object
activeInfo: null, // provider info (name, icon, rdns)
lastError: null,
};
const els = {
tag: document.getElementById("tag"),
message: document.getElementById("message"),
submit: document.getElementById("submit"),
chipWallet: document.getElementById("chip-wallet"),
chipChain: document.getElementById("chip-chain"),
txPanel: document.getElementById("tx-panel"),
txStatus: document.getElementById("tx-status"),
txHash: document.getElementById("tx-hash"),
diagEth: document.getElementById("diag-eth"),
diag6963: document.getElementById("diag-6963"),
diagSelected: document.getElementById("diag-selected"),
diagOrigin: document.getElementById("diag-origin"),
diagErr: document.getElementById("diag-err"),
diagContainer: document.getElementById("diag-container"),
pickerOverlay: document.getElementById("picker-overlay"),
pickerList: document.getElementById("picker-list"),
pickerCancel: document.getElementById("picker-cancel"),
segButtons: document.querySelectorAll(".seg-btn"),
};
// ─── ABI encoding (unchanged, verified byte-for-byte against eth_abi) ─
const pad32 = (hex) => hex.padStart(64, "0");
const encUint = (n) => pad32(BigInt(n).toString(16));
const encInt = (n) => pad32(BigInt.asUintN(256, BigInt(n)).toString(16));
const encString = (s) => {
const bytes = new TextEncoder().encode(s || "");
const lenHex = pad32(BigInt(bytes.length).toString(16));
let bodyHex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
bodyHex += "00".repeat((32 - (bytes.length % 32)) % 32);
return lenHex + bodyHex;
};
const encBytes32 = (hex) => (hex || "").replace(/^0x/, "").padStart(64, "0");
function encodeGiveFeedback({ agentId, value, decimals, tag1, tag2, endpoint, feedbackURI, feedbackHash }) {
const encTag1 = encString(tag1);
const encTag2 = encString(tag2);
const encEnd = encString(endpoint);
const encURI = encString(feedbackURI);
const headBytes = 8 * 32;
const offTag1 = BigInt(headBytes);
const offTag2 = offTag1 + BigInt(encTag1.length / 2);
const offEnd = offTag2 + BigInt(encTag2.length / 2);
const offURI = offEnd + BigInt(encEnd.length / 2);
const head = encUint(agentId) + encInt(value) + encUint(decimals)
+ pad32(offTag1.toString(16)) + pad32(offTag2.toString(16))
+ pad32(offEnd.toString(16)) + pad32(offURI.toString(16))
+ encBytes32(feedbackHash);
return "0x" + SELECTOR + head + encTag1 + encTag2 + encEnd + encURI;
}
// ─── EIP-6963 provider discovery ─────────────────────────────────────
// Modern standard (https://eips.ethereum.org/EIPS/eip-6963) that
// handles the "multiple wallet extensions fighting over window.ethereum"
// problem cleanly. Each wallet announces itself via a message event
// with full metadata (name, icon, uuid, rdns). We collect them all
// and present a picker.
function setupEip6963Discovery() {
window.addEventListener("eip6963:announceProvider", (event) => {
const detail = event.detail;
if (!detail || !detail.provider) return;
const existing = state.providers.find((p) => p.info.uuid === detail.info?.uuid);
if (!existing) {
state.providers.push({ info: detail.info, provider: detail.provider });
renderDiagnostics();
}
});
window.dispatchEvent(new Event("eip6963:requestProvider"));
// Some extensions announce lazily; re-poke after short delays
setTimeout(() => window.dispatchEvent(new Event("eip6963:requestProvider")), 300);
setTimeout(() => window.dispatchEvent(new Event("eip6963:requestProvider")), 1000);
}
// ─── Wallet picker modal ─────────────────────────────────────────────
function showPicker() {
const providers = state.providers;
els.pickerList.innerHTML = "";
if (providers.length === 0) {
// Fallback: show window.ethereum if no 6963 providers
if (window.ethereum) {
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = "window.ethereum (unknown wallet)";
btn.addEventListener("click", () => {
selectProvider({ info: { name: "Unknown (window.ethereum)", rdns: "unknown" }, provider: window.ethereum });
hidePicker();
});
els.pickerList.appendChild(btn);
} else {
const msg = document.createElement("div");
msg.style.color = "var(--muted)";
msg.style.fontSize = "11px";
msg.style.padding = "10px 0";
msg.textContent = "No wallet detected. Install Rabby, MetaMask, or Coinbase Wallet and reload.";
els.pickerList.appendChild(msg);
}
} else {
for (const { info, provider } of providers) {
const btn = document.createElement("button");
btn.type = "button";
if (info.icon) {
const img = document.createElement("img");
img.src = info.icon;
img.alt = "";
btn.appendChild(img);
}
const label = document.createElement("span");
label.textContent = info.name || info.rdns || "Unknown wallet";
btn.appendChild(label);
btn.addEventListener("click", () => {
selectProvider({ info, provider });
hidePicker();
});
els.pickerList.appendChild(btn);
}
}
els.pickerOverlay.classList.add("show");
}
function hidePicker() { els.pickerOverlay.classList.remove("show"); }
els.pickerCancel.addEventListener("click", hidePicker);
els.pickerOverlay.addEventListener("click", (e) => { if (e.target === els.pickerOverlay) hidePicker(); });
async function selectProvider({ info, provider }) {
state.activeProvider = provider;
state.activeInfo = info;
renderDiagnostics();
try {
const accounts = await provider.request({ method: "eth_requestAccounts" });
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);
updateUi();
} catch (err) {
state.lastError = friendlyErr(err);
renderDiagnostics();
updateUi();
}
}
function attachProviderListeners(provider) {
if (provider._probe_attached) return;
provider._probe_attached = true;
provider.on?.("accountsChanged", (accs) => {
state.address = (Array.isArray(accs) ? accs[0] : null) || null;
updateUi();
});
provider.on?.("chainChanged", (hex) => {
state.chainId = parseInt(hex, 16);
updateUi();
});
}
// ─── UI rendering ────────────────────────────────────────────────────
function renderDiagnostics() {
els.diagEth.textContent = window.ethereum
? `present (${describeEthFlavor()})`
: "undefined";
els.diagEth.className = "v";
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.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 (e.isPhantom) flags.push("phantom");
if (Array.isArray(e.providers)) flags.push(`providers[${e.providers.length}]`);
return flags.length ? flags.join(",") : "unknown flavor";
}
function shortAddr(a) { return a ? `${a.slice(0,6)}${a.slice(-4)}` : ""; }
function updateChips() {
if (state.address) {
els.chipWallet.textContent = `wallet · ${shortAddr(state.address)}`;
els.chipWallet.className = "chip ok";
} else {
els.chipWallet.textContent = "wallet · not connected";
els.chipWallet.className = "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 updateSubmitButton() {
// Button is ALWAYS clickable — we handle no-wallet state inside
// the click handler by opening the picker or showing an error.
if (!state.address) { els.submit.textContent = "connect wallet"; els.submit.disabled = false; return; }
if (state.chainId !== BASE_CHAIN_ID) { els.submit.textContent = "switch to base"; els.submit.disabled = false; return; }
if (state.txStatus === "pending") { els.submit.textContent = "submitting…"; els.submit.disabled = true; return; }
els.submit.textContent = "sign + send feedback"; els.submit.disabled = false;
}
function updateUi() {
updateChips();
updateSubmitButton();
renderDiagnostics();
}
// ─── Submit / connect / switch orchestration ─────────────────────────
async function onSubmitClick() {
// Open picker if not yet connected. Always show it — let the user pick.
if (!state.address) {
showPicker();
return;
}
if (state.chainId !== BASE_CHAIN_ID) {
await switchToBase();
return;
}
await sendFeedback();
}
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);
}
updateUi();
}
async function sendFeedback() {
const valueMap = { positive: 100, neutral: 0, critical: -100 };
const value = valueMap[state.sentiment] ?? 0;
const tag1 = els.tag.value || "overall";
const tag2 = state.sentiment;
const msg = (els.message.value || "").trim();
const feedbackURI = msg ? `data:text/plain;base64,${btoa(unescape(encodeURIComponent(msg)))}` : "";
const feedbackHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
const data = encodeGiveFeedback({ agentId: AGENT_ID, value, decimals: 2, tag1, tag2, endpoint: "", feedbackURI, feedbackHash });
state.txStatus = "pending"; state.txHash = null; state.txError = null;
renderTx(); updateSubmitButton();
try {
const txHash = await state.activeProvider.request({
method: "eth_sendTransaction",
params: [{ from: state.address, to: REG_ADDR, data, value: "0x0" }],
});
state.txHash = txHash;
renderTx();
pollForReceipt(txHash);
} catch (err) {
state.txStatus = "failed";
state.txError = friendlyErr(err);
state.lastError = state.txError;
renderTx(); renderDiagnostics(); updateSubmitButton();
}
}
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(); updateSubmitButton();
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 (/self-feedback not allowed/i.test(msg)) return "you own agent 2205 — use a different wallet";
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";
return msg.slice(0, 160);
}
// ─── Sentiment selector ──────────────────────────────────────────────
els.segButtons.forEach((b) => {
b.addEventListener("click", () => {
state.sentiment = b.dataset.value;
els.segButtons.forEach((x) => x.classList.toggle("active", x === b));
});
});
els.submit.addEventListener("click", onSubmitClick);
// ─── Init ────────────────────────────────────────────────────────────
(function init() {
setupEip6963Discovery();
renderDiagnostics();
updateUi();
// Don't auto-silently connect — picker is better UX. User clicks the button.
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment