DEV Community

Charlie Zhu
Charlie Zhu

Posted on

A Webhook, a Free Server, and a Token Ledger

A developer needed a small notification service. GitHub pushes had to become short messages in a private chat. The budget was zero, and the tolerance for maintenance was lower than the budget.

The obvious options were a hosted function or a cheap VPS. Both cost something, and both came with accounts, billing forms, and expiry dates. The actual task was boring: receive a webhook, verify it, summarize it, send it. That is a good size for an experiment, because a failure costs nothing and a success is visible immediately.

MonkeyCode's free tier offers access to coding models and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. At the time of writing, the free tier includes a 10-million-token allowance and a server that can run small services. The exact numbers matter less than the shape of the constraint: a token budget and a machine that may sleep.

The project was a commit digest webhook for one repository. The goal had three requirements. The service had to run unattended, survive restarts, and keep a visible token budget. The third requirement is the one most tutorials skip.

The first version of the code came from a coding model. It handled the happy path cleanly: parse the payload, join the commit messages, post to Telegram. It also skipped HMAC verification entirely. That is the edge where free AI code breaks. The fix is small, but the lesson is structural. Generated code needs a threat-model pass, not just a syntax check.

import hashlib
import hmac
import json
import os

import httpx
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SECRET = os.environ["GITHUB_WEBHOOK_SECRET"]
TELEGRAM_TOKEN = os.environ["TELEGRAM_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]


def verify_signature(payload: bytes, signature: str) -> bool:
    expected = "sha256=" + hmac.new(
        SECRET.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/github")
async def github_webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("X-Hub-Signature-256", "")
    if not verify_signature(payload, signature):
        raise HTTPException(status_code=401, detail="bad signature")

    if request.headers.get("X-GitHub-Event") != "push":
        return {"status": "ignored"}

    data = json.loads(payload)
    commits = data.get("commits", [])
    lines = [
        f"- {c['message'].splitlines()[0]} ({c['author']['name']})"
        for c in commits
    ]
    digest = (
        f"New commits on {data['repository']['full_name']}:\n"
        + "\n".join(lines)
    )

    async with httpx.AsyncClient() as client:
        await client.post(
            f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
            json={"chat_id": TELEGRAM_CHAT_ID, "text": digest},
        )
    return {"status": "ok", "commits": len(commits)}
Enter fullscreen mode Exit fullscreen mode

The HMAC check uses hmac.compare_digest, which resists timing attacks. The model did not produce that on the first try. It produced a plain string comparison, which is correct in appearance and wrong in practice.

Deployment used the free server option. The service runs behind a systemd unit, so a crash restarts it and a reboot brings it back. The unit file is short.

[Unit]
Description=commit-digest webhook
After=network.target

[Service]
WorkingDirectory=/opt/commit-digest
EnvironmentFile=/opt/commit-digest/.env
ExecStart=/opt/commit-digest/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

The token budget needs a ledger. A model call reports usage, but a webhook service makes many small calls, and the totals drift out of sight. The fix is a small middleware that logs an estimate for every request. Four characters per token is a rough heuristic for English text, and it is good enough to see trends.

import logging
import time

logger = logging.getLogger("token_ledger")


def estimate_tokens(text: str) -> int:
    return max(1, len(text) // 4)


@app.middleware("http")
async def ledger(request: Request, call_next):
    start = time.monotonic()
    response = await call_next(request)
    elapsed = time.monotonic() - start
    logger.info(
        "path=%s status=%s elapsed_ms=%.0f est_tokens=%d",
        request.url.path,
        response.status_code,
        elapsed * 1000,
        estimate_tokens(str(request.url.path)),
    )
    return response
Enter fullscreen mode Exit fullscreen mode

The test run used a real GitHub webhook pointed at the server. A sample push with twenty commits produced a digest that arrived in the chat in about a second. The ledger showed each event consuming a fraction of the allowance. The arithmetic is simple: a two-thousand-character payload is roughly five hundred tokens, so ten million tokens covers about twenty thousand events.

The server stayed up across the observation window. A restart took a few seconds to come back, which is fine for a webhook and fatal for a user-facing API. That is the real lesson of the case study. Free tiers fail at the edges, not at the center. One webhook works. Fifty concurrent requests, persistent storage, or a strict latency budget would not.

Who should not use this approach? Anyone running a service with an uptime promise, a database that must survive restarts, or regulated data. The free server is a lab bench, not a production floor. It is excellent for experiments, prototypes, and small internal tools where a missed message is an annoyance rather than an incident.

The full script is above. If you want to run the same experiment, the free tier is enough to try it. The ledger will tell you when it is not.

MonkeyCode provides free models that can run this workflow.

Top comments (0)