This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
My day job is message queues in containers. The first rule there is boring and absolute: a message isn't acknowledged until it's safely stored. Lose power a millisecond later and it's still there.
Webhooks don't work like that. Stripe or GitHub sends your app a message, and if the app is down, slow, or chokes on a field that was renamed overnight, the message is usually just gone. The person who finds out is the shop owner, two days later, asking why three payments never showed up.
Holdfast is that queue rule, turned into a product for the people who get that phone call: freelance developers and small agencies who look after integrations for a handful of clients. It sits between the provider and the app and:
- Saves every webhook to disk before saying "got it".
- Checks the signature, and keeps forged or replayed messages aside.
- Delivers patiently: retries when the app is down, holds when a retry won't help.
- Explains what broke in two voices: a plain sentence the client can read, and a precise fix for the developer. Gemma writes both; code checks every word.
- Sends held messages again, never twice.
Who pays: the freelancer who maintains "the Stripe thing" for five small clients. One lost payment webhook is a lost order and an awkward conversation. Holdfast turns that into a note they can forward:
Kabir's βΉ1050 payment for Ginger chai, 500 g couldn't reach Chai & Co. because the sender changed how it sends customer information. Holdfast is keeping the message safe, and it can be sent again once the app is updated to expect the new format.
That's a real answer from the deployed site, written by Gemma 3 4B running on Render.
Demo
Live: https://holdfast-o9ba.onrender.com
It has two modes, switchable from the header:
-
Demo (
/demo, no sign-up): Chai & Co., a pretend tea shop with made-up customers. Seven buttons break it on purpose: the shop goes down, a field is renamed, a payment arrives twice, a signature is forged, an old payment is replayed. The relay, retries, checks and explanations are the real engine. -
Live (
/live): sign in with GitHub, pick one of your repositories, and Holdfast adds itself as the webhook. GitHub's test ping arrives within seconds. If you don't have an app, deliver to Holdfast's built-in test app and switch it to "Server down" or "Turns messages away" to watch a real webhook get held, explained and resent.
One honest note: Gemma runs on two CPU cores, so an explanation takes up to a minute and a half (48 s and 83 s in my last two live runs). The page shows the built-in rules' answer instantly and swaps in Gemma's when it's ready.
Code
Tanay3484
/
holdfast
A safety net for webhooks: stores every event, holds what your app fails on, explains why with an open-weight model, and resends safely.
Holdfast
A safety net for webhooks. Holdfast sits between a provider (Stripe, GitHub) and your app. It stores every event before acknowledging it, holds the ones your app fails on, explains why in plain words with an open-weight model, and resends them safely once the problem is fixed.
Built for the Hacktoberfest 2026 Open-Source AI Challenge, Week 2: Side Hustle.
How it works
provider βββΆ /in/{route} βββΆ check signature βββΆ save to disk βββΆ "got it"
β
deliver to your app ββββββββββββββ
ββ accepted β done
ββ worth a retry β wait a little longer each time
ββ won't work β hold it safely
β
code collects the facts β (signature, every try, what changed since the last success)
Gemma names the cause from a fixed list and explains it; code checks every word
resend: never twice, never anything that failed its signature check
- Code decides, the model explains.β¦
Go, about 5,900 lines plus 1,550 lines of tests (59 tests). Every feature has a spec in specs/ with numbered requirements.
How I Built It
Open-weight model: Gemma 3 4B, served by Ollama in a private service next to the app. Stack: Go standard library, SQLite (pure-Go driver), server-rendered HTML, a little vanilla JS. Hosting: a single Render Blueprint (more below).
Code decides, the model explains
The rule I held to: the model never decides what happens to a message. Retries, holds, duplicate detection and resends are plain, tested Go. Gemma gets the facts code collected (signature result, every delivery attempt, the app's error, what changed in the payload) and does two jobs:
- pick a cause from a fixed list of eight, under a JSON schema, and
- write the explanation.
Then code checks what it wrote. The client summary is rejected and replaced with the built-in rules' wording if it contains jargon ("HTTP", "payload", "endpoint", an error number) or promises something nobody did. Titles always come from code.
The bug that taught me the most
First version, renamed-field scenario. Gemma got the cause right, but the client summary said:
A payment notification failed to reach the Chai & Co. system. The notification is being kept safe and reviewed.
Nobody was reviewing anything. Worse, the developer note said the app "now requires customer_id", which is backwards: Stripe changed, the app didn't.
The fix wasn't a better prompt alone. It was moving more work into code:
-
Code finds the rename. A shape-diff walks both JSON bodies and spots that
data.object.customer_iddisappeared whiledata.object.customer.idappeared. That's one rename, with a direction. Gemma is told "the sender now sends X where it used to send Y; the app did not change" instead of a raw list of paths. - The summary has a required shape: what happened to this payment (who, how much), why in everyday words, what happens next.
-
A
Promisecheck rejects "being reviewed", "will resend", "our team isβ¦". - The developer fix must name the field that moved, or the rules' fix is used.
After the fix, on the same scenario:
For your developer: The sender now uses
data.object.customer.idinstead ofdata.object.customer_id. Change the app to expectdata.object.customer.id.
Model size, measured
| Model | Where | Result on the renamed-field case |
|---|---|---|
| Gemma 3 1B | laptop | Wrote "the app will resend the held events" (a promise) and told the sender to change. The checks now catch both and fall back to the rules' answer. |
| Gemma 3 4B | laptop GPU | Named the customer and amount, explained the change plainly, and pointed at the exact field. About 16 s. |
| Gemma 3 4B | Render, 2 CPUs | Same quality. 92 s for one explanation. |
| Gemma 3 4B | Render, after the fixes below | Two live runs, both explained by Gemma: 48 s and 83 s. |
On a laptop CPU with two threads, one explanation was 740 prompt tokens read in 15 s and 115 tokens written in 14 s. That's why the page shows a rules answer first.
How I built it: an unlock game
I built this with Claude Code, spec first. The four core pieces started as stubs I had to write by hand, each a "level" with known-answer checks. go run ./cmd/unlock prints a progress board, and while a level was locked, every part of the website it powered showed a "WIP: write this file to reveal how this will look" tile. Writing a file literally revealed part of the site.
Being honest about who wrote what (it's recorded in tasks.md):
- Level 1, signatures: I wrote GitHub's HMAC check. Stripe's was started by me and finished by Claude.
-
Level 2, retry policy: written by me (backoff,
Retry-After, the hold rules), polished by Claude. - Levels 3 and 4, shape diff and resend plan: written by Claude at my request. Level 3 was a worked example to teach me recursion, which I'd stalled on, and level 4 was the last night before the deadline.
Two safety details I'm glad I didn't skip
-
Server-side request forgery. Live mode lets you type the URL Holdfast forwards to. Without care, anyone could point it at
holdfast-ollama:11434or a cloud metadata address and read the answers on the event page. Destinations are checked when saved and every connection is checked again at dial time, so a hostname that later resolves to a private IP is still refused. - Duplicates. Providers deliver at least once. The resend plan sends each event once, in arrival order, never one that was already delivered, and never one whose signature failed.
Why Does Open Innovation Matter?
- The data is the business's customers. Payment webhooks carry names, amounts and addresses. With an open-weight model on the same private network, none of it goes to an AI company, and the freelancer can tell their client that truthfully.
- No per-message bill. A small shop's integration might send hundreds of events a day. One small model on one server explains every failure for a flat cost.
- I could look inside the failure. When Gemma promised a review that wasn't happening, I could see exactly what it was given, change what code tells it, and constrain its output with a JSON schema. The fix was engineering, not hoping a vendor's model changes.
-
Swappable. The model is one setting (
HF_MODEL). I measured 1B against 4B and chose on evidence.
My Agent Session
Built pair-programming with Claude Code: spec first (brief, requirements, design, tasks, each approved by me), then implementation task by task, with subagents for coaching me on my levels, writing specs, and drafting this post. The coach was told never to write my code, only give hints and review.
My First User
My first user is a real app with a real repository: Tasklet, a small todo app I built (source) that turns GitHub issues into todos. It's strict on purpose, like a real handler, and it has a maintenance switch that makes it answer 503. I signed in to Holdfast's Live mode with GitHub, connected Tanay3484/tasklet, and pointed it at Tasklet's webhook address. My coding agent ran the rest from the command line while I was out, and I checked the results when I got back.
| Step | What happened |
|---|---|
| Connect the repo | Holdfast added the webhook. GitHub's test ping was acknowledged in 0.27 s. |
| Open issue #1 | GitHub β Holdfast acknowledged in 0.82 s. The todo appeared in Tasklet within 4 s. |
| Tasklet in maintenance, open issue #2 | Holdfast tried 5 times in 34 s (waiting 2, 4, 8, 16 s), got 503 each time, and held it. Gemma explained it in 40 s. |
| Close issue #1 | The todo was ticked off within 4 s. |
| Maintenance off, then "Send all held again" | I pressed it in Live mode. Issue #2's todo appeared in Tasklet, once. Nothing was lost while the app was down. |
One detail I'll own: Tasklet runs on Render's free plan, which doesn't keep a disk, so its todos reset whenever it falls asleep. That's why issue #1's todo had disappeared by the time I resent #2. Holdfast's copy of every message was still there.
Did anyone pay? No. My first user is me and my own app, so I won't claim a sale. What the run convinced me of is the thing I'd charge for: I switched an app off, opened an issue, and nothing was lost, and I got a sentence explaining why it was held that I could forward to a client. What I'd build next: an email or Slack alert the moment something is held, one-click Stripe sign-in, and a share link for the client note.
Something real from deploy day: on the first deploy, every Gemma call failed with connection refused on holdfast-ollama:10000. Ollama was healthy on port 11434, but Render's service-to-service address used its default port. The rules took over automatically, so the site stayed fully usable, which is exactly the fallback I designed for. One line in render.yaml (PORT=11434) fixed it.
Then two more, the same afternoon:
-
Out of memory. Render killed the Gemma service. Inside the container, llama.cpp sees the whole host (60+ GB, 16 CPUs), not my 4 GB plan, so it kept a prompt cache that could grow to 8 GB and a second, repacked copy of the weights. Three
LLAMA_ARG_*settings (no prompt cache, no repack, 2 threads) brought it back under 4 GB. -
A runaway answer. Some diagnoses ran on for hundreds of tokens and never closed their JSON. I reproduced it on my laptop's CPU with Render's exact settings: in 3 of 5 runs Gemma tried to end a string with a curly quote (
β}), which JSON treats as ordinary text, so it never stopped and started repeating itself. A second cause: Go wrote the JSON format's fields alphabetically (cause, fix, summary), so Gemma poured the whole story into the developer's fix and left the owner one line. The fix: length limits on each field, code that cuts a field at the strayβ}, a check that the summary ends like a sentence, and the fields listed as cause, summary, fix. Before: 2 of 5 runs usable. After: 4 of 4, in 16β40 s.
Each time, the visitor still got a correct, plain-language answer from the rules. Gemma makes the explanation better; it was never the thing keeping a payment safe.
What isn't built: one-click Stripe sign-in. Stripe Connect needs a Stripe platform account, which I don't have, so Stripe connects by hand. You paste one address into Stripe's webhook settings and its signing secret into Holdfast. The site says so plainly.
Prize Categories
- Best Use of Render. Render is the AI runtime: one Blueprint runs the public Go service (with a persistent disk for the event store) and a private Ollama service with Gemma baked into the image, talking over Render's private network. The model is never exposed to the internet.
- Best Use of Gemma. Gemma 3 4B writes every explanation and client note. Its output is constrained to a schema and checked by code, and I measured it against 1B to choose the size.



Top comments (0)