DEV Community

Riley Zhang
Riley Zhang

Posted on

Weekend Build Log: A Weekly Digest Bot With a Token Budget Ledger

It is Saturday, 9pm. Your nightly jobs failed forty times this week. Nobody read the emails. You want one readable digest before Monday.

That is the whole project. One input, one output, one scheduled run. This build log keeps it that small.

1. Write the cut list before the code

Most weekend projects die from a second feature. So decide what you will not build, in writing.

In scope Out of scope Why the cut
One weekly digest file Slack bot, email, web UI Delivery is solved; formatting is the real work
Failures from one pipeline Multi-repo aggregation Doubles auth surface for zero added insight
A model call to group failures A model call to fix failures Fixes need context you should not send
One cron entry A scheduler service A cron line survives reboots and code review

If a task is not in the left column, it waits until next weekend. Say that out loud once.

2. Fix the output shape before the input

Your digest has to be readable in fifteen seconds. So define the file first, not the parser.

# Nightly digest - {{week}}

## Needs a human ({{n}})
- {{job}} - first seen {{first_seen}} - {{signal}}

## Already known noise ({{noise_count}})
- {{pattern}} x {{count}}

## Not triaged ({{skipped_count}})
- {{job}} - {{reason}}
Enter fullscreen mode Exit fullscreen mode

Commit that as digest.template.md. Now your tests can assert on structure instead of vibes. The third section is not decoration. It is how you avoid pretending coverage you do not have.

3. Build the fixture before you touch the network

Record one real API response into fixtures/failures.json. Strip tokens, emails, and hostnames first. Then run everything from the file.

# fixture_run.py
import json, pathlib, subprocess

FIXTURE = pathlib.Path('fixtures/failures.json')
OUT = pathlib.Path('out/digest.md')

def load():
    jobs = json.loads(FIXTURE.read_text())
    assert isinstance(jobs, list) and jobs, 'fixture must be a non-empty list'
    return jobs

if __name__ == '__main__':
    jobs = load()
    print(f'fixture jobs: {len(jobs)}')
    subprocess.run(['python', 'digest.py', '--dry-run'], check=True)
    text = OUT.read_text()
    assert text.startswith('# Nightly digest'), 'template drifted'
    assert '@' not in text, 'redaction failed'
    print('fixture run OK')
Enter fullscreen mode Exit fullscreen mode

That last assert earns its keep. Redaction regressions are silent and expensive. One line catches them forever.

4. Guard the budget with a ledger

Free access is still a finite resource. A ledger is cheaper than regret, and it takes about fifteen lines.

# ledger.py
import json, os, pathlib, time

LEDGER = pathlib.Path('state/ledger.json')
CAP = int(os.environ.get('TOKEN_CAP', '120000'))  # per run, your own limit

def estimate(text):
    return max(1, len(text) // 4)  # rough, fine for a guard

def load():
    if LEDGER.exists():
        return json.loads(LEDGER.read_text())
    return {'runs': []}

def spend(prompt, completion):
    state = load()
    used = sum(r['tokens'] for r in state['runs'])
    cost = estimate(prompt) + estimate(completion)
    if used + cost > CAP:
        raise SystemExit(f'budget guard tripped: {used}+{cost} > {CAP}')
    state['runs'].append({'at': time.time(), 'tokens': cost})
    LEDGER.write_text(json.dumps(state, indent=2))
Enter fullscreen mode Exit fullscreen mode

Now test the only thing that matters for cost. Force the guard to trip.

TOKEN_CAP=1 python digest.py --dry-run; echo "exit=$?"
Enter fullscreen mode Exit fullscreen mode

You want a non-zero exit and no output file. If the digest still appears, your guard runs after the call, not before it.

5. Constrain the model with a plain contract

Long prompts invite creative formatting. Keep the contract short and boring.

prompt.txt

You group failing CI jobs.
Return JSON with one key, groups, a list of objects.
Each object has signal, jobs, and first_seen.
One group per distinct error signature.
No prose. No markdown fences.
Enter fullscreen mode Exit fullscreen mode

Then validate the reply instead of trusting it.

def parse(reply):
    data = json.loads(reply)
    groups = data.get('groups')
    if not isinstance(groups, list):
        raise ValueError('shape mismatch')
    return groups
Enter fullscreen mode Exit fullscreen mode

Parse, retry once, then fall back to a raw job list. The fallback is not failure. It is the path that keeps Monday intact.

6. Ship one cron entry and stop

This workload is one process per week with no inbound traffic. The free server option covers that shape comfortably. Check the project page for current limits rather than trusting a blog post.

# crontab -e
0 6 * * 1 cd /srv/digest && TOKEN_CAP=120000 python digest.py >> log/run.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Keep the log. It is your only audit trail when a digest looks wrong. Rotate it monthly if it grows.

Decision table: triage it or skip it

Situation Action Reason
Same signature three weeks running Group, hide Known noise, no new information
Logs contain tokens or personal data Skip, name only Sending them leaks credentials
Single flaky timeout Group, no model call The pattern is obvious already
New signature right after a deploy Send to a human first Highest information value per row
Malformed reply twice Raw list fallback The digest must never block

Where free access actually earns its place

This digest is small, weekly, and by design not urgent. Paying for capacity to run it would be hard to justify, and hobby budgets are real. Free model access plus a free server option is what keeps this a weekend build instead of a subscription decision. That is the entire role the product plays here.

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

One honest caveat. Free tiers change. Your ledger, your fixture, and your fallback path are what keep the project alive when a quota does.

What I skipped on purpose

  • No web UI. Terminals survive; dashboards get abandoned.
  • No auto-fix. A wrong fix costs more than a failed job.
  • No vector store. Signature grouping is enough at forty rows.
  • No cross-repo auth. One pipeline, one token, one blast radius.
  • No custom model routing. One call, one contract, one validator.

Who should not build this

Skip it if your logs contain customer data you cannot redact. Skip it if you need alerts within minutes, because weekly cron is the wrong tool. Skip it above a few thousand rows, where error signatures stop clustering. And skip it if you cannot accept that a free tier may change without notice.

The Monday check

Three commands, run in order, before you trust the cron entry.

python fixture_run.py
TOKEN_CAP=1 python digest.py --dry-run; echo "exit=$?"
tail -n 20 log/run.log
Enter fullscreen mode Exit fullscreen mode

All three pass, you schedule the job. Any of them fail, you keep the fallback and fix nothing else.

Your backlog does not need a platform. It needs one file, one guard, and one Monday morning. Start with the free tier, then tell me which section of the digest you ended up deleting.

Top comments (0)