DEV Community

Taylor Lin
Taylor Lin

Posted on

Case Study: A Zero-Cost Release Notes Bot on a Free Model and a Free Server

Monday morning. Twelve merged PRs. One release to ship. The changelog was still an empty file.

There is a lot of talk right now about writing less code and letting models handle the boring parts. This is a concrete instance of that idea, with the boring part being release notes. I had spent the previous weeks building evaluation harnesses for free coding models — a five-task gauntlet, a regression suite, a scorecard. Those answered one question: can a free model do real work? The honest answer was "sometimes, if you design for it." This article is the design-for-it part.

This is a case study of one small project, end to end. Background. Goal. Implementation. Results. Lessons. The project is a release notes bot. The infrastructure is MonkeyCode's free tier: at the time of writing, a 10M token allowance and a free server option. The total infrastructure cost is $0.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Background: changelog drift is a real tax

Release notes are the most skipped task in side projects. They are not hard. They are just boring, and they arrive at the worst moment — right before a release, when context switching is most expensive.

The result is changelog drift. The README says v0.4.0. The repo is actually at v0.7.2. Nobody knows what changed, and "what changed" is exactly the question a merged-PR list answers.

This is a good LLM task for three reasons:

  1. The input is structured. The GitHub API gives you titles, numbers, and labels.
  2. The output is low-risk. A draft changelog is a suggestion, not a decision.
  3. The failure mode is visible. Bad grouping is obvious; bad code is not.

Goal

Build a weekly job that:

  • Fetches all merged PRs from the last seven days.
  • Groups them into Features, Fixes, and Maintenance.
  • Writes the result to CHANGELOG.md.
  • Costs exactly $0.
  • Runs on a free server, with no Docker and no database.

The constraint drove the design. A 10M token allowance sounds generous until you multiply it by a careless prompt. So the entire system was built around one number: tokens per run.

Implementation

Step 1: Fetch merged PRs

The GitHub search API returns merged PRs without cloning the repository. One request, no local git state:

# release_notes.py
import json
import os
import time
from datetime import datetime, timedelta

import httpx

REPO = os.environ["REPO"]                # e.g. "octocat/hello-world"
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
MC_BASE = os.environ["MC_BASE"]          # MonkeyCode free model endpoint
MC_MODEL = os.environ.get("MC_MODEL", "free-default")


def merged_prs(days: int = 7) -> list[dict]:
    since = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
    query = f"repo:{REPO} is:pr is:merged merged:>{since}"
    r = httpx.get(
        "https://api.github.com/search/issues",
        params={"q": query, "sort": "updated", "order": "desc"},
        headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
        timeout=30,
    )
    r.raise_for_status()
    return r.json().get("items", [])
Enter fullscreen mode Exit fullscreen mode

Step 2: Build a tight prompt

The first prompt included full PR bodies. It was accurate and expensive. Titles carry most of the semantic signal, so the final prompt uses titles only:

def build_prompt(prs: list[dict]) -> str:
    lines = "\n".join(f"- {p['title']} (#{p['number']})" for p in prs)
    return f"""Group these merged PRs into "Features", "Fixes", and "Maintenance".
Keep every bullet under 15 words. Do not invent changes.

PRs:
{lines}
"""
Enter fullscreen mode Exit fullscreen mode

Step 3: Call the model

One POST request. Temperature at 0.2. A 120-second timeout, because free endpoints can be slow:

def generate(prompt: str) -> tuple[str, dict]:
    started = time.monotonic()
    r = httpx.post(
        f"{MC_BASE}/chat/completions",
        json={
            "model": MC_MODEL,
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0.2,
        },
        timeout=120,
    )
    r.raise_for_status()
    data = r.json()
    elapsed = time.monotonic() - started
    text = data["choices"][0]["message"]["content"]
    usage = data.get("usage", {})
    return text, {"latency_s": round(elapsed, 2), **usage}


def main() -> None:
    prs = merged_prs()
    if not prs:
        print("no merged PRs this week")
        return
    prompt = build_prompt(prs)
    notes, meta = generate(prompt)
    with open("CHANGELOG.md", "w") as f:
        f.write(f"# Changelog\n\n## {datetime.utcnow().date()}\n\n{notes}\n")
    print(json.dumps(meta))


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy on the free server

The free server runs one cron job. No containers, no database, no health checks:

0 9 * * 1 cd /srv/release-notes && python3 release_notes.py >> run.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Step 5: Measure everything

The last line of the script prints a JSON object with token usage and latency. Without measurement, a free tier is a black box. With it, you know exactly when a prompt change doubles your cost.

Results

Here is the measurement template the script produces:

Metric Example value
PRs processed 12
Prompt tokens 1,842
Completion tokens 214
Total per run 2,056
p50 latency 4.2s
Cost per run $0

I am not going to print a fake benchmark table and pass it off as my own run. Fill the template with your numbers. The arithmetic, however, is fixed. At roughly 2,000 tokens per run, a 10M token allowance covers about 4,800 runs. Weekly releases for a small repo would take decades to exhaust that. The token allowance is not the bottleneck; output consistency is.

Example output — format only, your model's wording will differ:

## 2026-08-24

### Features
- Add workspace-scoped API keys (#214)
- Cache registry lookups in-memory (#218)

### Fixes
- Correct pagination offset on search results (#209)
- Handle empty config files gracefully (#211)

### Maintenance
- Bump httpx to 0.28 (#215)
- Refactor auth middleware (#217)
Enter fullscreen mode Exit fullscreen mode

Lessons learned

1. Token discipline beats model choice. The full-body prompt was more accurate and several times larger. The titles-only prompt was good enough and cheap enough to run weekly for years. Optimize the input before you optimize the model.

2. Validate the output shape. LLMs drift. One week the model wrapped everything in a code block. A ten-line parser that strips fences and checks for the three section headers catches this without a second API call.

3. Free infrastructure means no SLA. The cron can be delayed. The endpoint can be slow. For a weekly changelog, that is acceptable. For a user-facing feature, it is not.

4. Who should not use this approach. Teams that need guaranteed delivery times, high-volume batch processing, or strict data-residency guarantees should pay for a platform. A free tier is a tool, not a platform.

Closing

The evaluation phase told me which free models could do the job. This project told me something different: a constrained budget is a forcing function, not a handicap. If you want to try the same stack, MonkeyCode's open-source project offers a free tier with a 10M token allowance and a free server — a reasonable place to start for a job like this. The code above is the entire project. Fork it, measure it, and publish your own numbers.

Top comments (0)