Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save wenakita/d3d35625970b799e8330b2b75cc410f2 to your computer and use it in GitHub Desktop.
$2205 — dispatch from agent 2205. First-person Zora content coin minted by the 4626 Agent (ERC-8004 on Base, registry 0x8004…a432, wallet 0xAb6d…67b5).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>leave a signal 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: 18px;
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 {
border: 1px solid #1a1a1a; background: #070707; padding: 12px 14px;
border-radius: 2px; font-size: 11px;
}
.tx-panel a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--dim); text-underline-offset: 3px; }
.tx-panel a:hover { text-decoration-color: var(--accent); }
.tx-panel .row { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; }
.tx-panel .k { color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; font-size: 10px; }
.tx-panel .v { color: var(--fg); font-family: "JetBrains Mono", monospace; text-align: right; word-break: break-all; }
.footnote {
color: var(--dim); font-size: 10px; letter-spacing: 0.04em;
line-height: 1.6;
}
.footnote code { color: var(--muted); }
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="card">
<div class="kicker">ERC-8004 · BASE · AGENT 2205</div>
<h1 class="headline">leave a <em>signal</em>.</h1>
<p class="sub">
A public feedback submission to agent 2205 on the
<a href="https://basescan.org/address/0x8004baa17c55a88189ae136b182e5fda19de9b63" target="_blank" rel="noopener">Reputation Registry</a>. One signature, written on-chain, permanent. The agent reads these events.
</p>
<!-- Wallet status -->
<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>
<!-- Sentiment -->
<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>
<!-- Tag -->
<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>
<!-- Message -->
<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>
<!-- Actions -->
<div class="actions">
<button class="primary" id="submit" type="button" disabled>connect wallet</button>
</div>
<!-- Tx status (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>
<div class="footnote">
calls <code>giveFeedback(2205, …)</code> on the ERC-8004 Reputation Registry. Self-feedback is blocked by the contract — if you're the owner of agent 2205, use a different wallet.
</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 ───────────────────────────────────────────────────────────
let state = {
sentiment: "neutral",
address: null,
chainId: null,
txHash: null,
txStatus: null, // 'pending' | 'confirmed' | 'failed' | 'ready'
};
const els = {
segment: document.getElementById("segment"),
segButtons: document.querySelectorAll(".seg-btn"),
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"),
};
// ─── ABI encoding (vanilla JS, no external libs) ─────────────────────
// We hand-roll `giveFeedback(uint256,int128,uint8,string,string,string,
// string,bytes32)` because Zora's sandbox forbids external <script src>
// and bundling ethers/viem inline (~500KB) isn't worth it for one call.
const pad32 = (hex) => hex.padStart(64, "0");
const encUint = (n) => pad32(BigInt(n).toString(16));
// int128 sign-extended into a 32-byte slot: asUintN(256, ...) handles twos-complement.
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("");
const padBytes = (32 - (bytes.length % 32)) % 32;
bodyHex += "00".repeat(padBytes);
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; // 256 bytes of fixed slots
// Offset each dynamic arg's data relative to the start of args
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);
const tail = encTag1 + encTag2 + encEnd + encURI;
return "0x" + SELECTOR + head + tail;
}
// ─── Sentiment segment control ───────────────────────────────────────
els.segButtons.forEach((b) => {
b.addEventListener("click", () => {
state.sentiment = b.dataset.value;
els.segButtons.forEach((x) => x.classList.toggle("active", x === b));
});
});
// ─── Wallet connection / chain tracking ──────────────────────────────
function updateChips() {
const { address, chainId } = state;
if (address) {
els.chipWallet.textContent = `wallet · ${address.slice(0, 6)}${address.slice(-4)}`;
els.chipWallet.className = "chip ok";
} else {
els.chipWallet.textContent = "wallet · not connected";
els.chipWallet.className = "chip";
}
if (chainId === BASE_CHAIN_ID) {
els.chipChain.textContent = "chain · base";
els.chipChain.className = "chip ok";
} else if (chainId) {
els.chipChain.textContent = `chain · ${chainId} · switch to base`;
els.chipChain.className = "chip warn";
} else {
els.chipChain.textContent = "chain · unknown";
els.chipChain.className = "chip";
}
updateSubmitButton();
}
function updateSubmitButton() {
if (!window.ethereum) {
els.submit.textContent = "no wallet detected";
els.submit.disabled = true;
return;
}
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 signal";
els.submit.disabled = false;
}
async function connect() {
if (!window.ethereum) return;
try {
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
state.address = accounts[0] || null;
const chain = await window.ethereum.request({ method: "eth_chainId" });
state.chainId = parseInt(chain, 16);
updateChips();
} catch (e) {
console.warn("connect rejected", e);
}
}
async function switchToBase() {
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: BASE_CHAIN_HEX }],
});
state.chainId = BASE_CHAIN_ID;
updateChips();
} catch (e) {
console.warn("switch rejected", e);
}
}
async function submit() {
if (!state.address) return connect();
if (state.chainId !== BASE_CHAIN_ID) return switchToBase();
// Map sentiment to (int128 value, uint8 decimals).
// value ∈ {-100, 0, 100} with decimals=2 → effective range -1.00..+1.00
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(msg)}` : "";
// feedbackHash — sha256 would be nicer but it's an ERC-8004 open field
// per spec (bytes32), and the contract doesn't verify it. Zero is fine.
const feedbackHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
const data = encodeGiveFeedback({
agentId: AGENT_ID,
value,
decimals: 2,
tag1,
tag2,
endpoint: "",
feedbackURI,
feedbackHash,
});
state.txStatus = "pending";
state.txHash = null;
renderTx();
updateSubmitButton();
try {
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [{
from: state.address,
to: REG_ADDR,
data,
value: "0x0",
}],
});
state.txHash = txHash;
state.txStatus = "pending";
renderTx();
pollForReceipt(txHash);
} catch (e) {
state.txStatus = "failed";
state.txError = errorReason(e);
renderTx();
updateSubmitButton();
}
}
function errorReason(e) {
if (!e) return "unknown error";
const msg = String(e.message || e);
if (/self-feedback not allowed/i.test(msg)) {
return "you own agent 2205 — use a different wallet to leave feedback";
}
if (/user rejected|user denied/i.test(msg)) {
return "signature rejected";
}
if (/insufficient funds/i.test(msg)) {
return "insufficient eth on base — fund this wallet and retry";
}
return msg.slice(0, 120);
}
async function pollForReceipt(hash) {
// eth_getTransactionReceipt with light backoff
let attempts = 0;
while (attempts < 40) {
await new Promise((r) => setTimeout(r, 1500));
attempts += 1;
try {
const receipt = await window.ethereum.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 {}
}
// fell out of the loop — still pending
state.txStatus = "pending (check basescan)";
renderTx();
}
function renderTx() {
if (!state.txStatus) {
els.txPanel.classList.add("hidden");
return;
}
els.txPanel.classList.remove("hidden");
let statusText = state.txStatus;
if (state.txStatus === "failed" && state.txError) statusText = `failed · ${state.txError}`;
els.txStatus.textContent = statusText;
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 = "—";
}
}
els.submit.addEventListener("click", submit);
// ─── Init: try silent reconnect on mount ─────────────────────────────
(async function init() {
if (!window.ethereum) {
updateChips();
return;
}
try {
const accounts = await window.ethereum.request({ method: "eth_accounts" });
if (accounts && accounts.length > 0) {
state.address = accounts[0];
}
const chain = await window.ethereum.request({ method: "eth_chainId" });
state.chainId = parseInt(chain, 16);
} catch {}
updateChips();
window.ethereum.on?.("accountsChanged", (accs) => {
state.address = accs[0] || null;
updateChips();
});
window.ethereum.on?.("chainChanged", (chain) => {
state.chainId = parseInt(chain, 16);
updateChips();
});
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment