Thanks @wslyvh for the explanation. Polling had already started and I can’t add Lion as an option now.
We should only count real users. There are bots voting for flamingo.
If you want to DM me on any suspected bot votes. I did some manual spot checks using admin function and didn’t see anything jump out.
We’ve had some people from the r/ethereum daily community vote who may not have had an account here before.
Just a quick message to state that I’m not a bot, even though I just joined to vote for the glamorous flamingo!
flamingo looks like it is ready for anything. Flamingo.
I propose to ban voting for Bear
We had 5 years of bear
Mascot of the greatest chain and the SOV should never be a bear.
Flamingo is cute and colorful.
P.S. Bear vote is also botted by memecoin traders since it was the least liked option hence biggest R/R.
Poll is closed
========== SUMMARY ==========
Duplicate votes excluded (same reg IP + same creation date): 13 — voted: 13 Flamingo
Votes excluded for account created after 2026-07-10: 63 — voted: 39 Flamingo, 24 Polar bear
Total excluded: 76 of 189 votes
Adjusted result: Flamingo 50 vs Polar bear 63
Verification script to generate the summary (run as mod)
// Mascot runoff integrity check — applies the announced eligibility rules:
// 1. Votes from accounts created after CUTOFF_DATE don't count
// 2. Votes from accounts sharing a registration IP AND creation date don't count
//
// Run in devtools console while logged in as mod at ethereum-magicians.org.
// Read-only. Can be run mid-poll (progress check) or after close (final tally).
//
// Set RUNOFF_POST_ID below. Option IDs are auto-discovered from the post.
(async () => {
// ======== CONFIG ========
const RUNOFF_POST_ID = 63602; // runoff poll is on the original first post
const POLL_NAME = "poll2"; // the runoff poll (original closed poll is "poll")
const CUTOFF_DATE = "2026-07-10"; // accounts created after this don't count
// ========================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const getJSON = async (url) => {
const res = await fetch(url, { headers: { Accept: "application/json" }, credentials: "include" });
if (res.status === 429) { console.warn("rate limited, waiting 30s…"); await sleep(30000); return getJSON(url); }
if (!res.ok) return { __status: res.status };
return res.json();
};
// 1. Discover poll options from the post
const post = await getJSON(`/posts/${RUNOFF_POST_ID}.json`);
const poll = (post.polls || []).find((p) => p.name === POLL_NAME);
if (!poll) { console.error("No poll found on that post. Check RUNOFF_POST_ID / POLL_NAME."); return; }
const optionName = {};
for (const o of poll.options) optionName[o.id] = o.html.replace(/<[^>]+>/g, "").trim();
console.log("Poll options:", Object.values(optionName).join(" | "), `— status: ${poll.status}, voters: ${poll.voters}`);
if (!poll.public) console.warn("⚠️ Poll is not public — voter lists won't be available. Make the runoff a public poll.");
// 2. Collect voters per option
const voters = new Map(); // id -> {username, opt}
for (const oid of Object.keys(optionName)) {
let page = 1;
while (true) {
const d = await getJSON(`/polls/voters.json?post_id=${RUNOFF_POST_ID}&poll_name=${POLL_NAME}&option_id=${oid}&page=${page}&limit=50`);
const batch = (d.voters && d.voters[oid]) || [];
if (!batch.length) break;
for (const u of batch) voters.set(u.id, { username: u.username, opt: optionName[oid] });
if (batch.length < 50) break;
page++; await sleep(300);
}
await sleep(300);
}
console.log(`${voters.size} voters — fetching account data…`);
// 3. Fetch registration IP + creation date for each voter
const rows = [];
let i = 0;
for (const [id, v] of voters) {
i++;
const d = await getJSON(`/admin/users/${id}.json`);
if (d.__status) {
if (i === 1) { console.error(`Got ${d.__status} — your account may lack IP visibility.`); return; }
rows.push({ id, username: v.username, opt: v.opt, error: d.__status });
} else {
rows.push({
id, username: v.username, opt: v.opt,
reg_ip: d.registration_ip_address || "",
created: (d.created_at || "").slice(0, 10),
});
}
if (i % 20 === 0) console.log(`…${i}/${voters.size}`);
await sleep(400);
}
// 4. Apply rule 1: created after cutoff
const lateAccounts = rows.filter((r) => r.created && r.created > CUTOFF_DATE);
// 5. Apply rule 2: same registration IP + same creation date (2+ accounts)
const groups = new Map(); // "ip|date" -> rows
for (const r of rows) {
if (!r.reg_ip || !r.created) continue;
const key = `${r.reg_ip}|${r.created}`;
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(r);
}
const dupGroups = [...groups.values()].filter((g) => g.length > 1);
const dupUsers = new Set(dupGroups.flat().map((r) => r.username));
// Informational: shared IP but different creation dates (NOT excluded)
const byIP = new Map();
for (const r of rows) {
if (!r.reg_ip) continue;
if (!byIP.has(r.reg_ip)) byIP.set(r.reg_ip, []);
byIP.get(r.reg_ip).push(r);
}
const sharedIPDiffDate = [...byIP.values()].filter(
(g) => g.length > 1 && new Set(g.map((r) => r.created)).size > 1
);
// 6. Tallies
const excluded = new Set([...lateAccounts.map((r) => r.username), ...dupUsers]);
const tally = (rs) => {
const t = {};
for (const r of rs) t[r.opt] = (t[r.opt] || 0) + 1;
return t;
};
const raw = tally(rows);
const adjusted = tally(rows.filter((r) => !excluded.has(r.username)));
// 7. Report
console.log("\n========== RAW TALLY ==========");
console.table(raw);
console.log(`\n========== RULE 1: created after ${CUTOFF_DATE} (${lateAccounts.length}) ==========`);
if (lateAccounts.length) console.table(lateAccounts.map((r) => ({ username: r.username, created: r.created, vote: r.opt })));
else console.log("None ✅");
console.log(`\n========== RULE 2: same reg IP + same creation date (${dupGroups.length} group(s)) ==========`);
if (dupGroups.length)
dupGroups.forEach((g, n) => {
console.log(`Group ${n + 1} (${g[0].created}):`);
console.table(g.map((r) => ({ username: r.username, vote: r.opt })));
});
else console.log("None ✅");
console.log("\n========== INFO ONLY: shared reg IP, different creation dates (not excluded) ==========");
if (sharedIPDiffDate.length)
sharedIPDiffDate.forEach((g) => console.table(g.map((r) => ({ username: r.username, created: r.created, vote: r.opt }))));
else console.log("None ✅");
console.log("\n========== ADJUSTED TALLY (official, per announced rules) ==========");
console.table(adjusted);
console.log(`Excluded votes: ${excluded.size} of ${rows.length}`);
// Summary suitable for posting in the thread
const breakdown = (rs) => {
const t = tally(rs);
return Object.entries(t).map(([o, n]) => `${n} ${o}`).join(", ") || "none";
};
const dupRows = rows.filter((r) => dupUsers.has(r.username));
const lateOnly = lateAccounts.filter((r) => !dupUsers.has(r.username)); // avoid double counting
console.log("\n========== SUMMARY ==========");
console.log(
`Duplicate votes excluded (same reg IP + same creation date): ${dupRows.length}` +
(dupRows.length ? ` — voted: ${breakdown(dupRows)}` : "")
);
console.log(
`Votes excluded for account created after ${CUTOFF_DATE}: ${lateOnly.length}` +
(lateOnly.length ? ` — voted: ${breakdown(lateOnly)}` : "")
);
console.log(`Total excluded: ${excluded.size} of ${rows.length} votes`);
console.log(
"Adjusted result: " + Object.entries(adjusted).map(([o, n]) => `${o} ${n}`).join(" vs ")
);
window.runoffAudit = { rows, raw, adjusted, lateAccounts, dupGroups };
console.log("\nFull data in window.runoffAudit");
})();
i mean posting on X and calling people for action, i.e. for voting for a particular mascot was wrong from day one, but yeah, it is what it is, and we are now stuck with a bear as a mascot,.
Apologies for anyone frustrated by this process. All errors in this are mine. I assumed that only meme coiners trying to distort the vote was going to cause any issues. I didn’t expect that there would be duplicate votes, or the level of passion.
We won’t use Eth Magicians polling for Mascot needed for Hegotá upgrade
Suggestions wanted for a better voting system.

