Heads up: This is an educational project. The bot can post to GitHub Discussions, which means you need to use it carefully. I'll cover what responsible use looks like at the end.
A few months ago I wanted to get GitHub's Galaxy Brain badge — the one you earn by getting answers marked as Accepted in Discussions. I figured automating the search part would be interesting. Finding relevant open questions manually is genuinely tedious.
What started as a script to find unanswered threads turned into a project I actually care about. Not because of the badge, but because of the infrastructure problems it forced me to solve.
Here's what's in the codebase and what I learned building each piece.
The basic idea
The bot does four things:
- Discovers GitHub repos that have Discussions enabled, filtered by topic tags and star count
- Fetches open, unanswered Discussion threads from those repos
- Sends the question to a free LLM via OpenRouter and gets an answer
- Optionally posts that answer (with your confirmation, by default)
None of that is complicated. The interesting problems came from making it reliable.
Problem 1: Free LLM models are flaky
OpenRouter's free tier is great — 18+ models, no billing required. The catch is availability. A model that worked yesterday might 404 today. A model that worked an hour ago might be rate-limited now.
My first version just tried one model and crashed when it failed. The fix was a rotation list:
_DEFAULT_MODELS = [
"qwen/qwen3.6-plus:free",
"stepfun/step-3.5-flash:free",
"nvidia/nemotron-3-super-120b-a12b:free",
"meta-llama/llama-3.3-70b-instruct:free",
"google/gemma-3-27b-it:free",
# ... 13 more
]
The bot tries them in order. If a model returns an empty response, a 404, or a 429, it logs the failure and moves to the next one. At session end, --models shows a table of which models succeeded, failed, and how fast they were.
One thing I noticed: "success" and "quality" are different. A model can return a response that passes all the checks but still gives a generic non-answer. The answer length filter (ANSWER_MIN_CHARS, ANSWER_MAX_CHARS) catches the worst cases, but it doesn't catch confident-sounding garbage.
Problem 2: Retrying the wrong way
Before adding proper backoff, the bot would hit a rate limit and immediately retry. Then retry again. Then again. GitHub's API would respond with 429 for the rest of the session.
The fix was reading Retry-After headers:
retry_after = int(response.headers.get("Retry-After", RATE_LIMIT_RETRY_AFTER_DEFAULT))
time.sleep(retry_after)
GitHub is inconsistent about including that header. When it's missing, the bot falls back to exponential delay. It's not perfect, but it's miles better than hammering an endpoint that's already telling you to stop.
Problem 3: One bad endpoint killing the whole session
This took me a while to notice. If a particular GraphQL query started timing out — say, because a repo was unusually large — the bot would retry it over and over, spending most of the session waiting.
Circuit breakers solve this. After a configurable number of consecutive failures, the circuit "opens" and blocks further requests to that endpoint for a timeout period. Then it half-opens: one probe request goes through. If that succeeds, the circuit closes again.
class CircuitBreaker:
CLOSED = "closed"
OPEN = "open"
HALF = "half_open"
def __init__(self, name, threshold=5, timeout=120):
self.state = self.CLOSED
self.failures = 0
self.opened_at = None
The effect in practice: the bot now skips broken endpoints and moves to the next repo in the list, instead of getting stuck.
Problem 4: Ctrl+C corrupting the stats file
The stats file (galaxy_brain_stats.json) gets written at the end of each session. If you hit Ctrl+C mid-write, you get a partial JSON file, which breaks the next session.
The fix was a ShutdownHandler that catches SIGINT/SIGTERM and sets a flag:
class ShutdownHandler:
def __init__(self):
self._shutdown = False
signal.signal(signal.SIGINT, self._handle)
signal.signal(signal.SIGTERM, self._handle)
def _handle(self, *_):
self._shutdown = True
@property
def requested(self):
return self._shutdown
Every loop in the bot checks shutdown.requested before its next iteration. Ctrl+C finishes the current task cleanly, then exits. The stats file always gets written to disk before the process ends.
What else is in there
In-memory TTL cache. Avoids re-fetching the same GraphQL results within a session. Default TTL is 5 minutes. When you're processing 50 repos, this matters.
Multi-modal support. If a Discussion post contains images and the selected model supports vision, the bot fetches the images and includes them in the prompt. Same for external links — it fetches the page content and appends it.
Key rotation. You can put multiple OpenRouter API keys in .env, comma-separated. When one hits its rate limit, the bot rotates to the next.
Webhook notifications. Discord and Slack. Sends an alert when an answer gets accepted, and a summary at the end of each session.
Stats by org. Breaks down your answer history by repository owner. Useful for seeing which communities you've been most active in.
Setting it up
git clone https://github.com/YOUR_USERNAME/galaxy-brain-bot.git
cd galaxy-brain-bot
pip install -r requirements.txt
cp .env.template .env
# Fill in GITHUB_TOKEN, GITHUB_USERNAME, OPENROUTER_KEYS
python test_bot.py # run this before anything else
python galaxy_brain_bot.py
The test suite checks all your credentials and API connections before you run the main bot. Run it first.
By default, the bot asks for confirmation before posting each answer (AUTO_APPROVE_ANSWERS=false). Keep it that way until you've seen a few answers and trust the output quality on the repos you're targeting.
Responsible use
A few things I want to say directly:
Don't run it unsupervised at scale. The MAX_ANSWERS_PER_SESSION setting exists for a reason. Posting 50 answers in an hour is a good way to get flagged.
Read the answers before posting. LLMs are wrong sometimes. Posting a confident wrong answer in a GitHub Discussion is worse than posting nothing.
Check the repo's rules. Some repos have Codes of Conduct that explicitly restrict automated participation. The bot tries to detect this, but it can miss things.
It's a research tool, not a farming tool. The badge is a byproduct. If you're using this to post low-effort answers at volume, you're not using it for what it was built for.
Source code
Full source, setup instructions, and config reference: https://github.com/itxashancode/Galaxy-Brain-Automation
The whole thing is one Python file (galaxy_brain_bot.py) plus a test suite. No frameworks, no magic — just requests, rich for the terminal UI, and python-dotenv. If you want to understand a specific piece, it's all in one place.
Happy to answer questions in the comments. If you spot something in the circuit breaker or backoff logic that could be better, I'm genuinely interested.
Built for learning. MIT licensed.
Top comments (0)