Polar bear selected as mascot for Glamsterdam upgrade

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.

2 Likes

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.

1 Like

Just a quick message to state that I’m not a bot, even though I just joined to vote for the glamorous flamingo!

3 Likes

1 Like

flamingo looks like it is ready for anything. Flamingo.

1 Like

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.

3 Likes

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");
})();
2 Likes

While I strongly disagree with the winner and the process, it is what it is.

1 Like

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,.

1 Like

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.