Cookbook

Alert on DAO proposals closing soon

Governance deadlines are the one crypto event you cannot catch up on afterwards — a vote you missed is simply gone. Forty lines and a cron entry.

~15 min Python, stdlib onlyNo API key, no signup
/items/dao/
01

Fetch, then filter on the dates yourself

The documented ?active=true parameter returns nothing (see below), so the window is computed from starts_at and ends_at. That turns out to be the more useful shape anyway: "open right now" is a weaker question than "open right now and closing inside three days".

import json, urllib.request from datetime import datetime, timezone API = "https://api.alphaday.com/items/dao/" CLOSING_WITHIN_HOURS = 72 def fetch(limit=100): with urllib.request.urlopen(f"{API}?limit={limit}") as r: return json.load(r)["results"] def closing_soon(proposals): now = datetime.now(timezone.utc) out = [] for p in proposals: starts = datetime.fromisoformat(p["starts_at"].replace("Z", "+00:00")) ends = datetime.fromisoformat(p["ends_at"].replace("Z", "+00:00")) if not (starts <= now <= ends): continue hours_left = (ends - now).total_seconds() / 3600 if hours_left <= CLOSING_WITHIN_HOURS: out.append((hours_left, p)) # Sort on the number alone. A bare sorted() falls through to comparing the # dicts whenever two proposals share a deadline, and then it raises. return sorted(out, key=lambda pair: pair[0]) for hours, p in closing_soon(fetch()): print(f"{hours:5.1f}h left {p['source']['name']:<14} {p['title'][:52]}")
02

Run it on a schedule

Proposals open and close continuously, so hourly is enough and daily misses short votes. Keep the hash of everything you have already alerted on, or you will re-send the same proposal every hour for three days.

0 * * * * /usr/bin/python3 /opt/alerts/dao_alerts.py

What it prints

Produced by running the code above against the live API, not written by hand.

31.1h left Gitcoin [Proposal]: Upgrade the Gitcoin Governor 63.2h left Arbitrum DAO Banning projects identified in the high-severity Wat 63.2h left Arbitrum DAO Banning projects identified in the high-severity Wat 63.2h left Arbitrum DAO Banning projects identified in the high-severity Wat 65.3h left Alchemix [AIP-124] Deprecate alUSD and alETH Bridging + Wind 65.3h left Alchemix [AIP-125] Launch on Base

Actual output, 15 Sep 2026. Those three Arbitrum rows are not a bug and not duplicates — they are three separate Snapshot proposals whose titles share a 52-character prefix. Widen the column or print the url; identify on hash, never on the title.

Variations

The same shape, pointed somewhere else. Every figure below came back from the live API.

Watch one DAO instead of all of them

tags=arbitrum pulls the whole Ethereum-adjacent ecosystem. sources=arbitrum_dao returns the 171 proposals that are actually Arbitrum's.

API + "?sources=arbitrum_dao&limit=100"

Alert on opening, not closing

Flip the window to starts_at within the last hour and you get a feed of proposals as they go live — better for a delegate who wants to read early than for a voter who wants a deadline.

if (now - starts).total_seconds() <= 3600: notify(p)

Post it somewhere

The alert list is plain text, so any webhook takes it. A Discord channel is the usual destination, and that is its own recipe.

urllib.request.urlopen(urllib.request.Request( WEBHOOK_URL, data=json.dumps({"content": line}).encode(), headers={"Content-Type": "application/json"}))

Before you build on this

?active=true returns nothing

The parameter is documented on /items/dao/ and it does not work: active=true returns 0 results, active=false returns all 6,603. Filter on starts_at and ends_at client-side instead, which is what the code above does. Reported to the API team.

period does not apply to governance

Proposals carry starts_at and ends_at rather than published_at, so a period filter passes through without narrowing anything. Window governance on its own dates.

Do not truncate titles to dedupe

Three separate Arbitrum proposals shared a 52-character prefix and looked like one row repeated. Distinct items have distinct hash and url values; use those to identify, and the title only to display.