# The Issue Digest Bot: A MonkeyCode Free-Tier Case Study

> Source: <https://dev.to/applab_8831/the-issue-digest-bot-a-monkeycode-free-tier-case-study-2b8o>
> Published: 2026-08-24 19:13:49+00:00

A maintainer of a small open-source library wakes up to forty notifications. Three are real bugs; the rest are duplicate questions, stale reports, and one pull request that is secretly an issue. The maintainer needs a triage summary before coffee, not after. The budget for tooling is exactly zero dollars.

That scenario is the background for this case study, and the current conversation about AI coding tools keeps circling one question: how much useful work can a free tier actually do? Instead of answering in the abstract, this article walks through one small project end to end. The goal is narrow: a bot that fetches open issues, summarizes them with a language model, and prints a short digest. The stack is deliberately boring — Python, the GitHub API, and MonkeyCode's free model access.

MonkeyCode is an open-source project that offers free model access and a free server option for small workloads. As of this writing, the free tier includes a 10 million token allowance. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The exact endpoint and model configuration are documented in the project README, and the script below reads them from environment variables so nothing is hard-coded.

The design decisions follow from the goal. Five issues per run keeps the prompt small and the response fast, and filtering out pull requests prevents the digest from mixing bugs with code reviews. Reading configuration from environment variables means the same script runs on a laptop and on the free server option without a single change. The whole project is two files: `digest.py`

and `test_digest.py`

.

``` python
# digest.py
import json
import os
import urllib.request

REPO = os.environ.get("GITHUB_REPO", "octocat/Hello-World")
TOKEN = os.environ.get("GITHUB_TOKEN", "")
MODEL_BASE = os.environ.get("MODEL_BASE", "")
MODEL_NAME = os.environ.get("MODEL_NAME", "")

def fetch_open_issues(repo, token):
    url = f"https://api.github.com/repos/{repo}/issues?state=open&per_page=5"
    request = urllib.request.Request(url)
    if token:
        request.add_header("Authorization", f"Bearer {token}")
    with urllib.request.urlopen(request, timeout=15) as response:
        return json.load(response)

def filter_issues(issues):
    return [issue for issue in issues if "pull_request" not in issue]

def summarize(issues, base, model):
    lines = "\n".join(f"- #{issue['number']}: {issue['title']}" for issue in issues)
    prompt = "Summarize these GitHub issues in three bullet points:\n\n" + lines
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.2,
    }
    request = urllib.request.Request(
        base.rstrip("/") + "/chat/completions",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(request, timeout=60) as response:
        data = json.load(response)
    return data["choices"][0]["message"]["content"]

def main():
    issues = fetch_open_issues(REPO, TOKEN)
    issues = filter_issues(issues)
    if not issues:
        print("No open issues.")
        return
    print(summarize(issues, MODEL_BASE, MODEL_NAME))

if __name__ == "__main__":
    main()
```

The first version of this script sent the entire issue body to the model. That was a mistake: issue bodies are long, noisy, and full of markdown that adds nothing to a three-bullet summary. The current version sends only the number and the title, so a digest of five issues fits in a few hundred tokens. That matters when the goal is to stay inside a free allowance for years rather than weeks.

The second file is the part that makes this project reproducible. Instead of calling a real model during tests, the test suite starts a fake model server on localhost. The server accepts any chat completion request and returns a canned response. This catches JSON parsing errors, wrong payload shapes, and timeout handling in seconds, without spending a single token.

``` python
# test_digest.py
import json
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer

from digest import filter_issues, summarize

class FakeModelHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        self.rfile.read(length)
        body = json.dumps(
            {"choices": [{"message": {"content": "summary from fake model"}}]}
        ).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *args):
        pass

class DigestTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.server = HTTPServer(("127.0.0.1", 0), FakeModelHandler)
        cls.port = cls.server.server_address[1]
        threading.Thread(target=cls.server.serve_forever, daemon=True).start()

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls.server.server_close()

    def test_filter_issues_removes_pull_requests(self):
        issues = [
            {"number": 1, "title": "Bug report"},
            {"number": 2, "title": "Add feature", "pull_request": {}},
        ]
        self.assertEqual(len(filter_issues(issues)), 1)

    def test_summarize_parses_model_response(self):
        issues = [{"number": 7, "title": "Login fails on Safari"}]
        base = f"http://127.0.0.1:{self.port}"
        result = summarize(issues, base, "fake-model")
        self.assertEqual(result, "summary from fake model")

if __name__ == "__main__":
    unittest.main()
```

Running the suite is one command:

```
python -m unittest test_digest.py -v
```

The fake server starts on a random port, serves the request, and shuts down. The test for the pull request filter uses a list with one real issue and one pull request, and asserts that only the issue survives. The test for the summarizer verifies that the response is parsed correctly. Both tests run offline.

A real run looks like this:

```
GITHUB_REPO=yourname/yourrepo python digest.py
```

The bot logs the remaining GitHub rate limit from the `X-RateLimit-Remaining`

response header. Unauthenticated requests are rate limited, and the exact number changes over time, so reading the header is more reliable than hard-coding a value. For a personal bot that runs a few times a day, the limit is a non-issue. For anything with more than a handful of users, it would be the first constraint to hit.

The results of this case study are best described as quiet. The test suite runs in under a second and spends zero tokens. A real digest costs one chat completion per run, typically a few hundred tokens, and at that rate a 10 million token allowance is effectively infinite for this workload. The constraint that actually shapes the project is not the token budget; it is the discipline of keeping the prompt small and the tests offline.

Three lessons came out of this project. First, mock the model before touching the real endpoint; the fake server catches integration mistakes in milliseconds, and it makes the test suite safe to run in CI without an API key. Second, send titles, not bodies; the model does not need the full text of an issue to produce a useful summary, and every unnecessary token is a small tax on the allowance. Third, a free server option changes the nature of a project, because a script that runs on a laptop is a demo while the same script running on a timer is a tool. MonkeyCode's free server option is what moves this bot from the first category to the second.

This approach is not for everyone. A team that needs guaranteed latency, high-throughput batch processing, or strict data residency should pay for an SLA and run on infrastructure they control. A free tier is a constraint, and constraints only help when they match the workload. For a personal bot that summarizes five issues a few times a day, the constraint is invisible; for a production pipeline, it would be the first thing to break.

The real output of this project is not the digest. It is the discovery that a small, well-tested bot can run on free infrastructure for a long time. The token allowance is generous, but the discipline required to stay inside it is the actual skill. Anyone with a small repository and a zero-dollar budget can copy this pattern and run it today; the current free-tier details in the MonkeyCode README are the only thing that needs checking before starting.
