The Silent $800 MRR Killer: Why I Built BillingWatch

The Day I Noticed the Drop

It started with a Slack notification I almost ignored: “Stripe: charge failed.”

One failed charge. Not unusual. I dismissed it and kept coding.

Three days later, I was reviewing my dashboard and noticed MRR had dropped by over $800. Not a gradual slope — a cliff. Eight subscribers had silently churned while I was building features. The charge failures had been piling up, Stripe had retried and failed, and not a single alert had fired in any tool I used.

That’s when I started building BillingWatch.

What “Silent Failures” Actually Look Like

The Stripe dashboard shows you events. But it doesn’t tell you when a pattern is wrong.

Here’s what I was missing:

  • Duplicate charges — same customer, same amount, within 60 seconds. Stripe retries are aggressive; idempotency keys are easy to get wrong.
  • Charge failure cascades — not one failed card, but five in an hour. That’s not bad cards. That’s a card-testing attack.
  • Lapsed subscriptions — customer.subscription.updated fires when a subscription lapses to past_due. Easy to filter out. Easy to miss for 3 days.
  • Negative invoice anomalies — unexpected credits creating negative line items. Fine if intentional. Silent bug if not.

None of these are events you can subscribe to cleanly. They’re patterns across events. And patterns require a monitor.

The Architecture That Solved It

BillingWatch is a FastAPI service that sits in front of your Stripe webhook endpoint. Every event flows through a detector engine before your app processes it.

from fastapi import FastAPI, Request
from billingwatch.detectors import run_all_detectors
from billingwatch.store import save_event

app = FastAPI()

@app.post("/webhook")
async def stripe_webhook(request: Request):
    payload = await request.body()
    event = stripe.Webhook.construct_event(
        payload, request.headers["stripe-signature"], STRIPE_WEBHOOK_SECRET
    )

    # Store first, then detect
    await save_event(event)
    alerts = await run_all_detectors(event)

    if alerts:
        await send_alerts(alerts)

    return {"received": True}

The detectors are composable. Each one takes an event and returns a list of alerts (or nothing). They can look at historical events to find patterns.

class DuplicateChargeDetector:
    async def detect(self, event: dict) -> list[Alert]:
        if event["type"] != "charge.succeeded":
            return []

        charge = event["data"]["object"]
        window_start = time.time() - 60  # 60-second window

        recent = await get_charges(
            customer=charge["customer"],
            amount=charge["amount"],
            since=window_start
        )

        if len(recent) > 1:
            return [Alert(
                level="warning",
                message=f"Duplicate charge detected: {charge['customer']} charged ${charge['amount']/100:.2f} twice in 60s",
                event_id=event["id"]
            )]
        return []

Run this on every charge.succeeded event and you’ll never miss a duplicate again.

What I Learned Building This

The hardest part wasn’t the code. It was figuring out which anomalies matter. I talked to five other SaaS founders. Everyone had a different “silent killer”:

  • One had a webhook endpoint that silently returned 200 but threw an exception internally
  • One had a subscription that stayed active 90 days after the credit card expired
  • One had card-testing attacks going undetected for weeks

BillingWatch ships with detectors for the most common patterns. But the real value is that you can write your own detectors for the failure modes specific to your business.

Ship It, Then Extend It

The core principle: monitor events, not just metrics.

Metrics tell you something went wrong. Event patterns tell you why — and often catch it before it shows up in metrics at all.

BillingWatch is open source. If you’re running Stripe at any scale and you’re not watching your webhook stream for anomalies, you’re flying blind. I built it because I had to. You probably will too — better to use something that already works.

→ BillingWatch on GitHub

Website Privacy Audit Checklist: 30 Things to Verify Before Your Next Compliance Review

A practical, do-it-yourself checklist for business owners who want to know exactly where their site stands on privacy — before hiring a consultant or paying for a tool.

Most privacy problems aren’t discovered by regulators first. They’re discovered by a developer who added a third-party script and didn’t tell anyone, or by a customer who noticed a tracker firing before they clicked “Accept.” By then, the damage is done.

A privacy audit doesn’t require a law degree or an enterprise compliance budget. It requires going through your site systematically and verifying — not assuming — that the basics are in place. This checklist gives you 30 specific things to check, organized into six areas. Work through it once and you’ll have a clear picture of where you stand and what needs fixing.

Section 1: Data Collection Inventory

Before you can comply with any privacy law, you need to know what data your website actually collects. Most business owners think they know — most are wrong. Developer-installed plugins, embedded widgets, and third-party scripts all collect data, often without the site owner’s knowledge.

  • [ ] Know every cookie set on your site (first- and third-party). Open your browser’s DevTools (Application > Cookies), load your site without consenting to anything, and list every cookie that’s already set. Note the name, domain, and purpose for each one. First-party cookies are set by your own domain; third-party cookies are set by external services.

  • [ ] Know every tracking pixel and script loading on your pages. In DevTools, open the Network tab, filter by “Script,” and reload your page. Every external script URL is a potential data collection point. Check your homepage, a product page, and your checkout or contact page — they often differ.

  • [ ] Know every third party receiving visitor data. In the Network tab, look for requests to external domains. Every domain that receives a request from your visitor’s browser is receiving some data (at minimum, the visitor’s IP address). List them. Common examples: Google, Meta, HubSpot, Hotjar, Intercom, Stripe, Cloudflare.

  • [ ] Know what data your forms collect and where it goes. Audit every form on your site — contact, newsletter, checkout, login, survey. For each one: what fields are collected, what service receives the submission, and where is it stored? If you’re using a form tool like Typeform, Gravity Forms, or HubSpot Forms, check what data it’s sending to the form provider’s servers.

  • [ ] Know what data is stored in local/session storage. In DevTools (Application > Local Storage / Session Storage), check what your site stores. Some analytics and marketing tools store identifiers here to track users across sessions or bypass cookie consent. If you find data you can’t explain, identify the source.

Section 2: Consent & Cookie Banner

A cookie banner that looks compliant and a cookie banner that is compliant are often very different things. The most common failure mode: the banner is visible but the cookies are already firing. Here’s how to verify yours actually works.

  • [ ] Non-essential cookies are blocked until consent is given — not just hidden. Before clicking anything on your banner, open DevTools > Network and reload. You should see zero requests to analytics or advertising domains. If Google Analytics fires before you click Accept, your banner is cosmetic, not functional.

  • [ ] Accept and Reject buttons are equally prominent. Inspect your banner. The Reject/Decline button must be as visible and accessible as the Accept button — same size, same contrast, not buried in a settings submenu. Pre-ticked boxes and misleadingly colored buttons are dark patterns that regulators are actively enforcing against.

  • [ ] Consent records are stored with timestamp and choices made. Test your consent flow and then check your backend or cookie provider’s dashboard. For each visitor who consents, you should have a record showing: when they consented, what choices they made (which categories), and what version of the banner they saw.

  • [ ] Visitors can change consent preferences at any time. Look for a consent preferences link in your site’s footer. It should reopen the banner and allow visitors to modify or withdraw their choices. “Withdraw consent” must be as easy as giving consent — not buried five clicks deep.

  • [ ] Google Consent Mode v2 signals are sent correctly (if using Google Ads or Analytics). If you run Google Ads or use GA4, Consent Mode v2 is required. Open Tag Assistant or check your GTM configuration — you should see gtag('consent', 'default', {...}) firing on page load before any other Google tags, with the appropriate ad_storage, analytics_storage, and ad_user_data parameters being updated based on visitor choice.

  • [ ] EU visitors get GDPR opt-in; California visitors get CCPA opt-out. Use a VPN or browser extension to simulate an EU IP address. Your banner should require opt-in (consent before cookies fire). Simulate a California IP — your banner should present an opt-out option (data is collected by default, but visitors can say no). These are different legal frameworks requiring different flows.

Section 3: Privacy Policy

Your privacy policy is a legal disclosure, not a formality. Regulators have specifically cited incomplete and generic privacy policies in enforcement actions. Check yours against these items.

  • [ ] Privacy policy exists and is accessible from every page. A link to your privacy policy should appear in the footer of every page — not just the homepage. Click through five pages on your site and verify the link is present and working on each one.

  • [ ] Policy names specific third-party services — not just “analytics providers.” Search your privacy policy for the word “analytics.” If it says “we use analytics providers to track site usage” without naming Google Analytics, Hotjar, or whatever you actually use, it’s incomplete. List every third-party service by name.

  • [ ] Policy covers all jurisdictions you serve. If you have EU customers, your policy must cover GDPR (legal bases for processing, DPA contact, EU data subjects’ rights). If you have California customers, it must cover CCPA (right to know, delete, and opt out of sale). Check whether your policy addresses each law’s specific requirements, not just privacy in the abstract.

  • [ ] Policy includes retention periods, legal bases (GDPR), and data rights. For each category of personal data you collect, your policy should state: how long you keep it, your legal basis under GDPR (consent, legitimate interest, contract, etc.), and what rights users have (access, deletion, portability, objection). These specifics are what transform a generic template into a compliant disclosure.

  • [ ] Policy is current — updated when your data practices changed. Check the “last updated” date. Now think: have you added any new tools, integrations, or features since that date? If HubSpot was added six months ago and your policy predates it, your policy is out of date. Regulators can and do cite stale policies.

Section 4: Data Subject Rights

GDPR gives EU residents the right to access, correct, delete, and port their data within 30 days of a request. CCPA gives California residents similar rights within 45 days. Having these rights in your policy means nothing if you can’t actually fulfill them.

  • [ ] There is a way for visitors to submit access and deletion requests. Your site needs a visible, functional mechanism for submitting data subject requests — a dedicated form, a clearly labeled email address, or a self-service portal. “Contact us” is not sufficient. Test it yourself: can you submit a request right now in under two minutes?

  • [ ] You know where personal data lives across all your systems. List every system that holds personal data: your CRM, email platform, analytics tool, payment processor, support desk, and any databases your app writes to. When a deletion request comes in, you need to be able to fulfill it across all of them — not just your main database.

  • [ ] You have a process to respond within 30 days (GDPR) / 45 days (CCPA). “Process” means: someone is notified when a request arrives, a deadline is tracked, and there’s a documented workflow for verifying identity, gathering data, and responding. A shared inbox with no owner and no deadline tracking is not a process.

  • [ ] Deletion requests are actually fulfilled across all systems — not just the main database. When you delete a user from your app, do you also delete their data from your email marketing list, your analytics platform, your support history, and your data warehouse? Test this end-to-end. Most companies find at least one system they forgot.

Section 5: Security Basics

Privacy compliance and data security are separate disciplines, but they overlap in ways that matter for compliance. A breach caused by a preventable vulnerability is also a compliance failure.

  • [ ] HTTPS is enforced across the entire site — no mixed content. Navigate to your site using http:// (not https://). You should be automatically redirected. Open DevTools > Console on your main pages and look for “mixed content” warnings — these indicate that some resources are still loading over HTTP even when the page itself is HTTPS.

  • [ ] CMS and plugins are updated. Log into your CMS (WordPress, Webflow, Shopify, etc.) and check for pending updates. Outdated plugins are the most common vector for website compromises. If you’re running WordPress, check: WordPress core version, all active plugins, and your theme. A single unpatched plugin can expose every visitor record you hold.

  • [ ] Third-party scripts are loaded only from trusted CDNs. Review the external script URLs you found in Section 1. Each one should load from a domain you recognize and trust. Be alert to any scripts loading from unfamiliar domains, especially if they were added by a plugin you installed recently — script injection via compromised plugins is a real attack vector.

  • [ ] Payment pages don’t capture card data before Stripe or PayPal processes it. If you use Stripe, card data should never touch your server — it goes directly from the visitor’s browser to Stripe’s servers via Stripe.js. Check your checkout page in DevTools: card field input should not appear in any form submission to your own domain. If it does, you have a serious compliance and security problem.

Section 6: Ongoing Monitoring

Privacy compliance isn’t a project with a completion date. Your website changes. You add tools, your developers install plugins, your marketing team embeds new widgets. Without ongoing monitoring, a site that passes this audit today can drift out of compliance within weeks.

  • [ ] New trackers are detected when developers add tools or plugins. Establish a process — or use a tool — that alerts you when a new tracker appears on your site. At minimum, someone should be running a scan after any significant deployment. The worst compliance surprises come from scripts that were added quietly and never disclosed to the privacy team.

  • [ ] Privacy policy is reviewed and updated at least annually. Schedule a recurring calendar event: once a year, compare your privacy policy against your actual data practices. Has anything changed? New integrations? New data flows? New jurisdictions you’re serving? Update the policy to match and update the “last updated” date.

  • [ ] There is a process for handling new DSARs when they arrive. This item is about readiness, not just tooling. If a data subject request arrived tomorrow, does someone know they own it? Do they know the deadline? Do they know where to look? A process that exists in someone’s head and isn’t documented will break the first time that person is unavailable.

How to Use This Checklist

Most items require actually loading your site — not reading documentation or trusting that something is set up correctly. Open your browser, open DevTools (F12 on most browsers), and verify each item directly.

A few items — like checking your consent record storage or your DSAR intake — require logging into your backend or third-party dashboards.

One-time fixes vs. ongoing monitoring:

The items in Sections 1–5 are primarily one-time fixes. Once you’ve inventoried your data collection, fixed your consent banner, updated your privacy policy, established a DSAR process, and addressed security basics, those items are done until something on your site changes.

Section 6 is different. Monitoring, policy reviews, and DSAR readiness are ongoing. They need to be owned by someone and built into your regular operations — not treated as a project that ends.

Prioritize by risk. If you haven’t done Section 2 (consent and cookie banner) or Section 3 (privacy policy), start there — these are the items regulators check first and where most enforcement actions originate. Section 1 (data inventory) is the foundation that makes everything else possible.

How Custodia Automates the Hard Parts

Sections 1, 2, and 6 of this checklist — data collection inventory, consent verification, and ongoing monitoring — are the most technically demanding and the most time-consuming to maintain manually.

Custodia handles all three automatically. A single scan crawls your site like a real visitor, detects every cookie, tracker, pixel, and third-party script, and maps exactly what fires before and after consent. Weekly re-scans catch new trackers the moment they appear. And if your consent banner has a gap — a cookie firing before opt-in, a missing Google Consent Mode signal — Custodia flags it with specific remediation steps, not just a generic warning.

The manual items in Sections 3, 4, and 5 — privacy policy accuracy, DSAR processes, security hygiene — are still on you. This post exists because compliance requires human decisions, not just automation. But if you’re spending hours manually running scanner tools and cross-referencing network requests, there’s a better use of your time.

Run a free privacy scan →

See every tracker on your site, verify your consent banner is actually blocking cookies, and get a plain-English compliance report — in about 60 seconds.

Last updated: March 2026

Mind-Bending Realities: 7 Famous Paradoxes That Still Baffle Scientists and Philosophers

Mind-Bending Realities: 7 Famous Paradoxes That Still Baffle Scientists and

Philosophers

Humanity has always sought to order the chaos of the universe through logic,
mathematics, and empirical science. Yet, throughout history, we have
encountered intellectual speed bumps—situations where our best tools for
reasoning seem to break down. These are paradoxes: statements or scenarios
that lead to a logical contradiction or a situation that defies intuition.
While some are playful riddles, others strike at the very heart of how we
understand existence, time, and consciousness.

What Makes a Paradox So Powerful?

A paradox is more than just a tricky question. It is a fundamental tension in
our worldview. When we encounter a true paradox, it forces us to choose
between two equally plausible but contradictory conclusions. Solving them
often leads to breakthroughs in physics, linguistics, or computer science.
Below, we dive into the most famous paradoxes that continue to puzzle both the
scientific community and the greatest philosophical minds of our age.

1. The Grandfather Paradox: Can Time Travel Break Reality?

Perhaps the most iconic of all science fiction tropes, the Grandfather
Paradox, poses a simple question: If you could travel back in time, could you
kill your own grandfather before he met your grandmother? If you succeeded,
you would never be born. If you were never born, you couldn’t travel back in
time to kill him. This infinite loop of logic has kept physicists awake for
decades.

  • The Logical Snag: The action invalidates the premise of the action.
  • Potential Solutions: Many physicists point to the Many-Worlds Interpretation, suggesting that killing your grandfather would simply create a new, divergent timeline, leaving your original past untouched.

2. Fermi’s Paradox: Where Is Everybody?

In 1950, physicist Enrico Fermi asked a simple question over lunch: “Where is
everybody?” Given the sheer number of stars in the galaxy and the high
probability that many of them host Earth-like planets, it seems inevitable
that intelligent extraterrestrial life should exist. Yet, we have found no
definitive evidence of it.

Why Does This Matter?

The lack of contact suggests several possibilities: either intelligent life is
incredibly rare, civilizations inevitably destroy themselves before they can
achieve interstellar travel, or we are simply looking in the wrong way with
the wrong technology.

3. Schrödinger’s Cat: The Quantum Weirdness

Erwin Schrödinger devised this thought experiment to illustrate the absurdity
of the Copenhagen interpretation of quantum mechanics. In the scenario, a cat
is placed in a sealed box with a radioactive source, a detector, and a vial of
poison. If a single atom decays, the detector triggers, releasing the poison.
According to quantum theory, the atom exists in a superposition of states—both
decayed and not decayed. Consequently, the cat is both alive and dead until
someone opens the box to observe it.

This highlights the massive divide between the quantum world of subatomic
particles and the macro world we experience daily.

4. The Ship of Theseus: The Question of Identity

If you have a wooden ship and you replace every single plank, nail, and sail
over time, is it still the same ship? This ancient Greek paradox forces us to
reconsider the nature of identity. If you take all the original parts and
rebuild a second ship, which one is the ‘original’?

This puzzle extends to the human body, where almost every cell in our system
is replaced over the course of a lifetime. Are you the same person you were at
age five?

5. Zeno’s Paradox: The Illusion of Motion

Zeno of Elea argued that motion is impossible. To get from point A to point B,
you must first travel half the distance. Then, you must travel half of the
remaining distance, then half of that, and so on. Because you are infinitely
dividing the space between points, you can never actually reach your
destination.

While calculus provides a mathematical answer to how an infinite series can
have a finite sum, Zeno’s paradox still serves as a profound meditation on the
nature of continuity versus discreteness in our physical world.

6. The Birthday Paradox: Probability Defied

This is a statistical paradox that catches almost everyone off guard. In a
room of just 23 people, there is a greater than 50% chance that at least two
people share the same birthday. If you increase the group to 70 people, the
probability rises to 99.9%.

Why is this a paradox? Because our intuition tells us that with 365 days in a
year, you would need hundreds of people to see a match. We fail to realize
that we aren’t looking for a specific match to a specific date, but any
match among the many possible pairs in the group.

7. The Liar Paradox: When Language Breaks

“This statement is false.” If it is true, then it must be false. If it is
false, then it must be true. This linguistic loop, famously studied by
Epimenides, shows that language itself can be used to construct a logical trap
that cannot be resolved within the same system of rules.

Conclusion: Embracing the Unknown

Paradoxes are the boundaries of human knowledge. They are not signs of
intellectual failure, but rather signposts pointing toward deeper truths about
the universe. Whether it’s the quantum state of a subatomic particle or the
social implications of extraterrestrial silence, these paradoxes keep the
spirit of inquiry alive. By trying to solve the unsolvable, we refine our
logic, expand our physics, and humbly accept that the universe is far stranger
than we could have ever imagined.

Frequently Asked Questions (FAQ)

Are paradoxes actually solvable?

Some, like the Birthday Paradox, are solvable through mathematics. Others,
like the Ship of Theseus, are philosophical questions where the answer depends
on how you define ‘identity’ rather than a hard calculation.

Why are paradoxes important for scientists?

Paradoxes expose flaws in our theories. If a theory leads to a paradox, it
means the theory is incomplete or that our initial assumptions were incorrect,
driving scientific progress.

Can a paradox be dangerous?

In a logical sense, yes. If a mathematical system contains a paradox, it can
lead to ‘explosion,’ where any statement can be proven true, rendering the
entire system useless. This is why mathematicians spend so much time ensuring
systems are consistent.

Is time travel actually possible based on these paradoxes?

While theoretical physics allows for solutions that don’t involve paradoxes
(like parallel universes), we have no experimental evidence that time travel
to the past is possible.

7 Mac Apps Every API-Heavy Developer Needs in 2026

If you spend most of your day hitting endpoints, debugging webhooks, or wrangling LLM APIs, your tooling matters more than most people realize. A good setup disappears into your workflow. A bad one adds friction to every single request.

Here are 7 Mac apps I rely on daily for API-heavy development — from debugging proxies to cost tracking to staying focused when you’re three hours deep in a rate-limiting rabbit hole.

1. Proxyman — See Every Request Your App Makes

Proxyman is the HTTP debugging proxy that finally feels native on macOS. It intercepts and displays all HTTP/HTTPS traffic from your apps, letting you inspect headers, payloads, and response times in a clean interface. If you’ve ever used Charles Proxy and wished it didn’t look like it was built in 2008, Proxyman is the upgrade. The breakpoint feature lets you modify requests and responses on the fly, which is invaluable for testing edge cases without touching your code.

Price: Free tier available, Pro from $69

2. Warp — A Terminal Built for API Workflows

Warp reimagines the terminal with features that make API work significantly faster. The AI command search helps you recall complex curl commands instantly, and the block-based output means you can scroll through API responses without losing track of which command produced which output. The collaborative features are great if you’re pair-debugging API issues with a teammate. It genuinely feels like what a terminal should be in 2026.

Price: Free for individuals

3. Raycast — Launch Anything, Query Anything

Raycast replaced Spotlight for me and never looked back. For API work specifically, the snippet expansion feature is a lifesaver — I have shortcuts for common auth headers, base URLs, and JSON templates that expand instantly. The extensions ecosystem includes direct integrations with GitHub, Jira, and Linear, so you can check PR status or create issues without leaving your keyboard. The clipboard history alone saves me from re-copying API keys dozens of times a day.

Price: Free, Pro from $8/mo

4. TokenBar — Know Exactly What Your API Calls Cost

TokenBar sits in your menu bar and tracks LLM token usage across every API call in real time. If you’re working with OpenAI, Anthropic, or any token-based API, this thing pays for itself immediately. You get a persistent count of input and output tokens right in your menu bar — no dashboard to open, no spreadsheet to maintain. I started using it after a month where my Claude API bill was 3x what I expected. Turns out a single runaway script was burning through tokens overnight. Never again.

Price: $5 lifetime

5. Insomnia — API Testing Without the Bloat

Insomnia is the API client I keep coming back to. While Postman has become increasingly heavy with cloud features and team collaboration tools, Insomnia stays focused on what matters: designing, debugging, and testing APIs. The environment variable system is clean, the GraphQL support is excellent, and it handles authentication flows (OAuth, API keys, bearer tokens) without making you jump through hoops. It also has solid Git sync for version-controlling your API collections.

Price: Free, paid plans from $5/mo

6. Monk Mode — Block the Feeds, Keep the Docs

Monk Mode does something clever that no other distraction blocker does: it blocks feeds within apps instead of blocking entire apps. When you’re deep in API debugging and you “just quickly check” Twitter or Reddit, you lose 20 minutes. Monk Mode kills the feed content while keeping the rest of the site functional — so you can still access Stack Overflow answers, GitHub issues, and API documentation without getting sucked into the infinite scroll. It’s the difference between blocking the internet and blocking the distractions.

Price: $15 lifetime

7. CleanShot X — Document Your APIs Properly

CleanShot X is the screenshot and screen recording tool that makes API documentation painless. The scrolling capture feature grabs entire API responses or long JSON payloads in a single image. The annotation tools let you highlight specific fields or headers before sharing with your team. I use it constantly for bug reports — a screenshot of the actual request/response in Proxyman communicates the problem faster than any written description. The quick recording feature is great for documenting complex multi-step API flows too.

Price: $29 one-time

The Stack in Action

These tools complement each other well. A typical debugging session for me looks like: Proxyman capturing traffic, Warp running curl commands, Raycast pulling up snippets, TokenBar keeping an eye on API costs in the menu bar, and Monk Mode making sure I don’t “take a break” that turns into an hour on social media. CleanShot captures whatever I need to share.

The goal isn’t to have the most tools — it’s to have the right ones so you spend more time building and less time fighting your environment.

What’s in your API development stack? Drop your must-haves in the comments.

Rethinking React routing: a simpler, more predictable approach

I built a modern React router because I was tired of existing ones

Routing in React has always felt… either too simple or too complicated.

  • Some routers feel too minimal — you end up building everything yourself
  • Others feel over-engineered — great power, but heavy mental overhead

So I built my own.

👉 Introducing routexiz — a lightweight, modern router for React with a clean mental model.

🧠 The idea: routing as a tree

Instead of thinking in flat routes, routexiz models your app like a tree.

Each route is a node.
Navigation resolves a single path from root to leaf.

route("/", Layout, root => {
  root.route("/dashboard", Dashboard, dash => {
    dash.guard(authGuard)
    dash.middleware(trackPageView)
  })
})

This gives you:

  • Natural nested layouts
  • Predictable execution
  • Clear structure as your app grows

⚡ Navigation that understands your route

Instead of this:

navigate("/users/1")

You write:

navigate("/users/:id", {
  params: { id: 1 }
})

No string guessing.
No mismatched paths.

🔥 What makes it different?

I focused on combining things I always wanted in one place:

  • Suspense-first data loading
  • Route-level guards (block navigation)
  • Middleware (side effects)
  • Prefetch (hover + viewport)
  • Loader caching with TTL
  • Error boundaries per route
  • Built-in transitions

All without making the API complex.

🧩 Example: data loading

route("/users/:id", User, {
  loader: async ({ params }) => fetchUser(params.id)
})

const data = useLoaderData()

Simple, predictable, and works with React Suspense.

🆚 How is this different?

Very roughly:

  • Traditional routers → string-based navigation
  • Some modern routers → very powerful, but complex mental model
  • routexiz → tries to balance power + simplicity

The goal is not to compete on features alone, but on developer experience and clarity.

🧠 Mental model

  • Guard → “Can we enter?”
  • Middleware → “Do something while entering”

This separation keeps things clean as your app grows.

🧪 Where I think it shines

From my own usage, this approach feels especially nice when:

  • Building dashboards with nested layouts
  • Handling auth / permissions cleanly
  • Prefetching data for smoother navigation
  • Keeping routing logic predictable as the app scales

🚧 Current state

I’ve built most of the core features:

  • Type-safe params
  • Nested routing builder
  • Devtools (basic)
  • React adapter
  • Documentation & examples

But…

👉 The ecosystem is still very early.

🙌 Looking for feedback (and contributors)

If this approach resonates with you, I’d love your feedback:

  • What feels intuitive?
  • What feels off?
  • What’s missing for production use?

Even better — try it in a small project and break it 😄

🔗 Links

🧠 Final thought

I’m not trying to replace existing routers overnight.

I just want to explore a simpler, more predictable way to handle routing in modern React.

If that sounds interesting to you — let’s build it together.

💬 If you’re currently using React Router or another solution,

what’s the biggest pain point you have with routing today?

Why Your Claude-Assisted Builds Break Down After Week 3 (And How to Fix It)

There’s a pattern I keep seeing with developers who use Claude to build things.

Week 1: Everything’s flying. Features ship fast. It feels like a superpower.

Week 2: Still fast, but you’re starting to notice context is getting messy.

Week 3: Something breaks. You ask Claude to fix it. Claude fixes it and breaks something else. You’re now spending more time explaining what was already built than actually shipping.

By week 4, some people stop using Claude entirely. Others power through but feel stuck in a loop.

Here’s what’s happening — and how to get out of it.

The real problem isn’t your prompts

Most people assume they’re bad at prompting. They go looking for better prompt templates, prompt chaining techniques, or ways to “jailbreak” better responses.

That’s the wrong diagnosis.

The actual problem is structural debt.

Every time you give Claude a new instruction without grounding it in how your codebase actually works, you’re building on a shaky foundation. Claude doesn’t automatically know:

  • How your files are organized
  • What decisions you made three sessions ago
  • Why certain patterns exist
  • What naming conventions you’ve established
  • What’s intentionally simple vs. what’s genuinely incomplete

Without that context, Claude is essentially building in the dark. It produces output that works locally but doesn’t integrate cleanly with what already exists.

The trust vs. verify trap

There’s a related issue: AI is fast, and that speed creates false confidence.

When you get a working feature in 30 seconds, your brain treats it like code you fully understand. But you didn’t write it — you approved it. And those are very different levels of comprehension.

The structural problems you’d normally catch while writing the code (because you were forced to think through each decision) don’t get caught because you’re skimming output instead of authoring it.

This isn’t a critique of Claude — it’s a workflow problem. The tool is doing exactly what you asked. The issue is that “write this feature” is a very different request from “write this feature in a way that’s consistent with how everything else in this project is built.”

What actually helps

The shift I’ve seen work best is this: stop treating each session as a fresh conversation.

Instead of explaining what you want Claude to build, start each session by grounding it in what already exists. Give it:

  • A brief architecture summary (what this codebase is, what patterns it follows)
  • The relevant existing code it needs to understand before making changes
  • Clear constraints (“don’t create new files when you can extend existing ones”)
  • Explicit acceptance criteria (“this is done when X, Y, Z are true”)

This sounds like more work upfront, but it dramatically reduces the rework cycle. You’re paying for context now instead of paying for debugging later.

The CLAUDE.md pattern

One practical technique: create a CLAUDE.md file at your project root. It doesn’t need to be long — even 20-30 lines can make a meaningful difference.

Include things like:

  • The project’s purpose in one sentence
  • Key architectural decisions and why you made them
  • Naming conventions
  • What Claude should avoid doing (avoid creating new API layers, prefer extending existing services, etc.)

When you start a new session, paste that file in before you start giving instructions. You’ll immediately notice Claude’s responses becoming more consistent and less likely to introduce patterns that don’t fit your codebase.

The deeper mental model shift

The builders who get consistent results with Claude aren’t necessarily better at writing prompts.

They’ve internalized that Claude is a collaborator that needs orientation, not a vending machine that dispenses features. Every time they start a session, they think: what does Claude need to know about this codebase before I give it a task?

That shift — from “request features” to “maintain shared context” — is what separates builds that compound over time from ones that become impossible to maintain.

A free resource

I built a short starter pack around this idea — 9 pages, completely free. It covers the core mental model shift, includes 5 prompt frameworks from a larger system I’ve developed, and shows how to structure your workflow so Claude builds things that stay maintainable.

If you’re hitting the week-3 wall, it might help: Ship With Claude — Starter Pack

No email required, no upsell. Just the thing that helped me.

What patterns have you noticed in your own AI-assisted builds? Curious what’s worked (or hasn’t) for people at different scales.

High p99 Latency in Go Service: Identifying and Resolving Bottlenecks to Prevent System Overload

cover

cover

Introduction: The Latency Challenge

In distributed systems, p99 latency often emerges as the silent killer of performance, despite healthy p50 and p95 metrics. This phenomenon is particularly acute in Go services, where the request lifecycle—from client initiation to load balancer routing and service processing—can be disrupted by straggler requests. These stragglers, consuming disproportionate resources, act as systemic bottlenecks, delaying subsequent requests and cascading into degraded user experience. The mechanical process here is straightforward: a single slow request, often due to resource contention or downstream dependency issues, holds up the goroutine scheduler, causing a backlog that amplifies tail latency.

Retries, a common mitigation strategy, proved ineffective—and in some cases, counterproductive. The causal chain is clear: retries increase load on already stressed resources, triggering retry storms that exacerbate latency. This is particularly evident in Go’s runtime, where garbage collection pauses and network variability compound the issue. The failure mechanism here is twofold: first, retries assume the problem is transient, but stragglers are often persistent; second, they lack awareness of system load, blindly adding pressure without resolving the root cause.

The breakthrough came from reframing the problem: stragglers, not failures, were the enemy. Hedged requests—sending a backup request after a timeout—emerged as a solution. The key challenge was determining the optimal hedging timeout without overloading the system. This decision point is critical: too short, and you waste resources on redundant requests; too long, and you miss the window to mitigate the straggler. The optimal timeout depends on queueing theory principles, balancing service time and request distribution to minimize tail latency without triggering resource exhaustion.

In practice, hedging reduced p99 latency by 74% in a real-world Go service, with minimal impact on p50 and a slight, expected increase in load. The implementation, packaged as hedge, demonstrates the effectiveness of this approach. However, hedging is not a silver bullet. It fails when downstream dependencies are the bottleneck, as parallel requests can saturate shared resources. The rule for choosing hedging is clear: if stragglers dominate tail latency and retries worsen load, use hedging with a timeout tuned to your request distribution. Missteps include over-triggering hedges, leading to unintended load spikes, and ignoring root causes like inefficient code or misconfigured load balancers.

Understanding the physical mechanics of latency—how requests queue, resources contend, and timeouts trigger—is essential. Without this, solutions remain superficial, addressing symptoms rather than causes. The stakes are high: unchecked p99 latency leads to system instability, operational inefficiencies, and user abandonment. As systems grow more complex, hedging stragglers offers a practical, measurable path to resilience—but only when applied with precision and awareness of the underlying mechanisms.

Diagnosing the Root Cause

Identifying the source of high p99 latency in our Go service required a systematic approach, combining trace analysis, statistical correlation, and controlled experimentation. The investigation revealed surprising insights into how straggler requests—not failures—were the primary culprits, despite initial assumptions.

Step 1: Isolating the Straggler Phenomenon

Using distributed tracing, we instrumented requests to map their lifecycle from client to response. The data showed that while p50 and p95 latencies were stable, p99 requests exhibited erratic behavior, often exceeding thresholds by orders of magnitude. These stragglers were not isolated incidents but systemic bottlenecks, consuming disproportionate CPU and memory resources due to Go’s goroutine scheduler being held up by slow requests.

Key observation: Retries exacerbated the issue, increasing load without resolving the root cause. This was confirmed by load testing, where retry storms triggered garbage collection pauses, further amplifying tail latency.

Step 2: Correlating Latency with Resource Contention

We employed statistical analysis to correlate latency spikes with resource utilization metrics. The results were striking: stragglers coincided with CPU saturation and network congestion, particularly in downstream dependencies. However, the surprising finding was that these issues were not due to failures but to persistent slow requests—a form of resource contention masked as transient errors.

Mechanism: Slow requests forced subsequent requests into a queueing backlog, where they waited for resources, creating a cascading delay effect. This was exacerbated by Go’s runtime characteristics, where goroutines blocked on I/O or synchronization primitives, stalling the scheduler.

Step 3: Testing Hedging vs. Retries

To address stragglers, we compared two strategies: retries and hedged requests. Retries, while intuitive, failed due to their blind addition of load without system load awareness. Hedging, however, reframed the problem by parallelizing backup requests after a timeout, effectively bypassing stragglers.

Experiment setup: We implemented hedging with a timeout threshold determined by queueing theory, balancing service time and request distribution. The results were dramatic—a 74% reduction in p99 latency with minimal impact on p50 and a slight, expected increase in load.

Edge case: Hedging failed when downstream dependencies were the bottleneck, as parallel requests saturated shared resources. This highlighted the importance of system load awareness in hedging timing.

Step 4: Validating the Solution with Chaos Engineering

To ensure robustness, we injected controlled stragglers using chaos engineering. The hedging strategy consistently outperformed retries, maintaining system stability under stress. However, we identified a critical failure mode: over-triggering hedges due to misconfigured timeouts, leading to unintended load spikes.

Rule for hedging: If stragglers dominate tail latency and retries worsen load, use hedging with a timeout tuned to request distribution. Avoid over-triggering by monitoring resource utilization and request queue lengths.

Surprising Findings and Practical Insights

  • Stragglers, not failures, are the primary drivers of tail latency. Retries are ineffective or counterproductive in these cases.
  • Hedging is a nuanced solution, requiring precise timeout thresholds and awareness of downstream dependencies.
  • Understanding the physical mechanics of latency—queuing, resource contention, and timeout triggers—is essential for effective solutions.

By addressing stragglers through hedged requests, we not only reduced p99 latency but also gained deeper insights into the systemic issues underlying tail latency in distributed systems.

Key Insights and Surprising Discoveries

The journey to reducing p99 latency by 74% in a Go service revealed several counterintuitive insights that challenged conventional wisdom. Here’s what stood out—and why it matters.

1. Stragglers, Not Failures, Drive Tail Latency

The most surprising discovery was that straggler requests, not transient failures, were the primary cause of high p99 latency. While retries are commonly used to handle failures, they exacerbated the problem by increasing load without addressing the root cause. Stragglers—requests that consume disproportionate resources due to resource contention or downstream dependency issues—create a queueing backlog. This backlog cascades delays, as Go’s goroutine scheduler becomes blocked by slow requests, stalling subsequent processing.

Mechanism: Slow requests hold onto CPU and memory, preventing the scheduler from efficiently allocating resources to other goroutines. This contention amplifies tail latency, as the system struggles to recover from the backlog.

2. Hedging Outperforms Retries by Reframing the Problem

The effectiveness of hedged requests was unexpected. By sending a backup request after a timeout, hedging addresses stragglers directly, rather than blindly retrying. This approach reduced p99 latency by 74% while leaving p50 mostly unchanged. The key was tuning the hedging timeout using queueing theory to balance service time and request distribution.

Mechanism: Hedging parallelizes the handling of slow requests, reducing the time spent waiting for stragglers. However, it fails when downstream dependencies are the bottleneck, as parallel requests saturate shared resources, leading to resource exhaustion.

3. Retries Are a Blunt Tool—and Often Counterproductive

Retries, while intuitive, were found to be ineffective or harmful. They assume transient issues but lack awareness of system load, often triggering retry storms that worsen latency. In Go, retries also coincide with garbage collection pauses, further degrading performance.

Mechanism: Retries add load without resolving the underlying straggler issue, leading to a feedback loop where increased load slows down the system further. This is particularly problematic in Go due to its runtime characteristics.

4. Optimal Hedging Timeout Is Critical—and Fragile

The success of hedging hinges on the timeout threshold. Too short, and it triggers unnecessary backup requests; too long, and it fails to mitigate stragglers. The optimal timeout depends on the request distribution and service time, requiring careful tuning.

Mechanism: The timeout must account for the point at which a request is likely to become a straggler. This is determined by analyzing the latency distribution and identifying the inflection point where requests deviate from typical performance.

5. Monitoring Resource Utilization Is Non-Negotiable

Hedging introduces a slight increase in load, which is expected. However, without monitoring CPU saturation, memory usage, and queue lengths, hedging can lead to unintended load spikes. This is especially risky in production, where resource limits are stricter.

Mechanism: Over-triggering hedges saturates resources, negating the benefits of reduced tail latency. Monitoring ensures that hedging remains within safe bounds, avoiding resource exhaustion.

Rule for Hedging: When to Use It—and When to Avoid

Use hedging if:

  • Stragglers dominate tail latency, and retries worsen load.
  • The hedging timeout is tuned to the request distribution.
  • Resource utilization is monitored to avoid over-triggering.

Avoid hedging if:

  • Downstream dependencies are the bottleneck, as parallel requests will saturate shared resources.
  • Root causes like inefficient code or misconfigured load balancers are unresolved.

Hedging is not a silver bullet but a precise tool for addressing stragglers. Its effectiveness depends on understanding the physical mechanics of latency—queuing, resource contention, and timeout triggers. When applied correctly, it transforms tail latency from a systemic issue into a manageable problem.

Implementation and Results

Addressing the high p99 latency in our Go-based service required a shift from traditional retry mechanisms to a more nuanced approach: hedged requests. The core issue wasn’t transient failures but straggler requests—slow responses that disproportionately consumed resources, blocking the goroutine scheduler and creating a queueing backlog. Retries, while intuitive, exacerbated the problem by increasing load and triggering garbage collection pauses in Go’s runtime, leading to a feedback loop of slower performance.

Hedging Strategy: Parallelizing Stragglers

We implemented hedged requests by sending a backup request if the primary request exceeded a calculated timeout. This timeout was determined using queueing theory, balancing the service time distribution and request latency inflection points. The mechanism works as follows:

  • Trigger Condition: A backup request is initiated when the primary request’s latency exceeds the calculated timeout, identified via latency distribution analysis.
  • Resource Impact: Parallel requests increase CPU and network load slightly, but this is offset by the reduction in tail latency. Monitoring resource utilization (CPU saturation, memory usage, and queue lengths) ensured hedging remained within safe bounds.
  • Outcome: This approach reduced p99 latency by 74%, with p50 remaining largely unchanged. The slight increase in load was expected and manageable, as hedging targeted only stragglers, not all requests.

Comparing Hedging vs. Retries: A Causal Analysis

Retries and hedging address different root causes. Retries assume transient failures, blindly adding load without resolving stragglers. Hedging, however, parallelizes handling of slow requests, reducing wait time for stragglers. The effectiveness of each depends on the system’s failure mode:

  • Retries: Optimal for transient network issues or intermittent failures. However, in our case, retries worsened latency by triggering retry storms and coinciding with garbage collection pauses.
  • Hedging: Optimal when stragglers dominate tail latency. It fails when downstream dependencies are the bottleneck, as parallel requests saturate shared resources.

Rule for Hedging: Use hedging if stragglers dominate tail latency and retries worsen load. Tune timeout to request distribution and monitor resource utilization to avoid over-triggering.

Edge Cases and Limitations

Hedging is not a silver bullet. It fails in scenarios where:

  • Downstream Bottlenecks: Parallel requests saturate shared resources (e.g., a database), negating latency benefits.
  • Misconfigured Timeouts: Over-triggering hedges due to poorly tuned timeouts causes unintended load spikes, amplifying resource exhaustion.

To mitigate these risks, we validated the hedging strategy through chaos engineering, injecting controlled stragglers to test system resilience. This revealed that hedging outperformed retries under stress but required precise timeout tuning and resource monitoring.

Practical Implementation and Broader Impact

The hedging mechanism was packaged into a reusable library, hedge, with minimal integration required:

client := &http.Client{ Transport: hedge.New(http.DefaultTransport),}resp, err := client.Get("https://api.example.com/data")

The broader impact of this change extended beyond latency reduction. By addressing stragglers, we improved system stability, reduced operational costs associated with inefficiencies, and enhanced user experience. Unchecked p99 latency had previously led to cascading delays, causing system instability and user abandonment. With hedging, the system became more resilient to tail latency spikes, maintaining competitive performance even under load.

Key Takeaways

  • Stragglers Drive Tail Latency: Focus on slow requests, not failures, to reduce p99 latency.
  • Hedging Requires Precision: Tune timeouts using queueing theory and monitor resource utilization to avoid over-triggering.
  • Retries Are Counterproductive: Address root causes (e.g., stragglers) instead of blindly adding load.

By understanding the physical mechanics of latency—request queuing, resource contention, and timeout triggers—we transformed a systemic issue into a measurable improvement, setting a new standard for performance optimization in distributed systems.

Lessons Learned and Best Practices

After dissecting the mechanics of high p99 latency in Go services, several actionable lessons emerged. These aren’t generic tips—they’re grounded in the physical and mechanical processes of request handling, resource contention, and system behavior under stress.

1. Stragglers, Not Failures, Drive Tail Latency

The root cause of high p99 latency in our case was straggler requests, not transient failures. Stragglers consume disproportionate resources (CPU, memory) due to Go’s goroutine scheduler being blocked by slow I/O or synchronization operations. This creates a queueing backlog, delaying subsequent requests. Rule: If p99 latency spikes while p50 remains stable, investigate stragglers before blaming failures.

2. Retries Worsen Latency by Amplifying Load

Retries are a blunt tool. In our case, they triggered retry storms, coinciding with garbage collection pauses in Go’s runtime. This created a feedback loop: more retries → more load → slower GC → higher latency. Rule: Avoid retries if stragglers dominate tail latency. Instead, address the root cause of slowness.

3. Hedging Outperforms Retries—But Only with Precision

Hedged requests reduced p99 latency by 74% by parallelizing backup requests after a calculated timeout. The key is tuning the timeout using queueing theory, balancing service time and request distribution. Rule: Use hedging if stragglers dominate and retries worsen load. Timeout must be precise; over-triggering hedges causes unintended load spikes. Edge Case: Hedging fails when downstream dependencies are the bottleneck, as parallel requests saturate shared resources.

4. Monitor Resource Utilization to Avoid Over-Triggering

Hedging increases load slightly, but without monitoring, it risks resource exhaustion. Over-triggering hedges saturates CPU, memory, or network, negating latency benefits. Rule: Monitor CPU saturation, memory usage, and queue lengths to keep hedging within safe bounds.

5. Understand the Physical Mechanics of Latency

Latency isn’t abstract—it’s a result of request queuing, resource contention, and timeout triggers. For example, Go’s goroutine scheduler stalls when slow requests block I/O, amplifying tail latency. Rule: Analyze latency distribution and correlate spikes with resource metrics to identify root causes.

6. Validate Solutions with Chaos Engineering

We validated hedging by injecting controlled stragglers and comparing it to retries under stress. Hedging outperformed retries but required precise timeout tuning and resource monitoring. Rule: Test solutions in realistic stress scenarios to uncover edge cases and limitations.

Practical Implementation

To apply these lessons, use the hedge library (available at https://github.com/bhope/hedge):

  • Minimal Integration: Wrap your HTTP client with hedging transport.
  • Timeout Tuning: Analyze latency distribution to identify straggler inflection points.
  • Monitoring: Track CPU, memory, and queue lengths to avoid over-triggering.

When Hedging Fails

Hedging isn’t a silver bullet. It fails when:

  • Downstream dependencies are the bottleneck: Parallel requests saturate shared resources.
  • Timeouts are misconfigured: Over-triggering hedges causes load spikes.
  • Root causes are ignored: Inefficient code or misconfigured load balancers persist.

Final Rule of Thumb

If stragglers dominate tail latency and retries worsen load → use hedging with precise timeout tuning and resource monitoring. Otherwise, address root causes like inefficient code or downstream bottlenecks.

Unchecked p99 latency leads to system instability, operational inefficiencies, and user abandonment. By understanding the mechanics and applying these lessons, you can transform latency issues into measurable performance optimizations.

Conclusion: The Path Forward

Digging into the root cause of high p99 latency in Go services revealed a surprising truth: stragglers, not failures, are the primary culprits. Traditional retries, often the go-to solution, exacerbated the problem by triggering retry storms and coinciding with garbage collection pauses, creating a feedback loop of increased load and slower performance. This is because retries add more requests to an already overloaded system, further blocking Go’s goroutine scheduler and amplifying tail latency.

The hedging strategy emerged as a superior alternative, reducing p99 latency by 74% in our case study. By parallelizing backup requests after a calculated timeout, hedging effectively short-circuits stragglers before they cascade into system-wide delays. However, its success hinges on precise timeout tuning—a misstep here can lead to over-triggering, saturating resources and negating latency gains. This is where queueing theory becomes indispensable, helping to identify the inflection point where a request transitions from “slow” to “straggler.”

Yet, hedging isn’t a silver bullet. It fails when downstream dependencies are the bottleneck, as parallel requests can saturate shared resources like databases. In such cases, addressing the root cause—whether inefficient code, misconfigured load balancers, or downstream bottlenecks—is paramount. Resource monitoring is equally critical; without tracking CPU saturation, memory usage, and queue lengths, hedging risks becoming a liability rather than a solution.

The key takeaway? Tail latency is a symptom of systemic issues, not isolated failures. Resolving it requires a curious, systematic mindset—one that combines trace analysis to pinpoint bottlenecks, chaos engineering to validate solutions under stress, and statistical analysis to correlate latency spikes with resource utilization. By approaching performance challenges with this rigor, we can transform latency issues into measurable optimizations, ensuring systems remain stable, efficient, and responsive even under load.

Rule for Hedging: Use hedging if stragglers dominate tail latency and retries worsen load. Tune timeout to request distribution, monitor resource utilization, and avoid if downstream dependencies are the bottleneck.

The path forward is clear: deep investigation pays off. By understanding the mechanics of latency, we can craft solutions that not only address symptoms but also fortify the system against future challenges. The potential for improvement is vast—and it starts with asking the right questions.

Ghost Maintainer — An AI Junior Partner for Open Source

Ghost Maintainer Notion Page

This is a submission for the Notion MCP Challenge

What I Built

I maintain a few open source projects solo, and honestly, the hardest part isn’t writing code — it’s everything around it. Triaging issues, figuring out which ones are bugs vs feature requests, reading through the codebase to understand what’s broken, and then actually getting around to fixing it. Most issues sit there for weeks.

Ghost Maintainer is my attempt to fix that. It’s an AI-powered system that turns Notion into an operations center for your GitHub repository. When someone files an issue, it automatically:

  1. Lands in a Triage Queue in Notion
  2. Gets classified by Gemini as a bug or feature request (with confidence scoring)
  3. Routes to the right backlog — Maintenance Backlog for bugs, Feature Backlog for feature requests
  4. If it’s a bug (and you have auto-fix on), it reads your entire codebase, proposes a fix, and opens a PR

The only thing left for you is reviewing the PR and hitting merge.

For bug reports, if you have disabled auto PR creation, then you trigger a fix using:

ghost_maintainer fix 3

For features, you trigger implementation when you’re ready:

ghost_maintainer implement 9

That kicks off the same pipeline — reads the codebase, writes the code, opens a PR.

The whole thing installs with one command from inside your repo:

curl -sL https://raw.githubusercontent.com/sbis04/ghost_maintainer/main/install.sh 
  -o install.sh && 
  bash install.sh && 
  rm install.sh

It asks for your Notion, GitHub, and Gemini tokens, then sets up everything — databases, secrets, workflows, the lot.

I also built a full MCP server (in Dart, using the dart_mcp package) that exposes 5 tools, 2 resources, and 2 prompts. You can connect it to Gemini CLI or Claude and interact with your maintenance backlog conversationally — triage issues, investigate bugs, deploy fixes, all through natural language.

The Notion workspace

Ghost Maintainer Notion Page

  • Triage Queue — every issue starts here, AI sorts it
  • Maintenance Backlog — confirmed bugs with AI summaries, priority, and PR links
  • Feature Backlog — feature requests waiting for implementation
  • Archive — completed items with resolved dates
  • Project Vision Statement — you edit this to guide how the AI thinks about your project

Feature Request Notion Page

Video Demo

Link to Code on GitHub

GitHub logo

sbis04
/
ghost_maintainer

AI-powered junior partner for solo open-source maintainers

Ghost Maintainer

ghost maintainer cover

Solo maintainers wear too many hats. Ghost Maintainer takes over the repetitive parts — triaging issues, reading code, writing fixes, and opening PRs — so you can focus on the work that actually needs a human.

It uses Notion as an operations center. Bugs get triaged and fixed automatically. Features queue up until you’re ready. Everything stays visible in Notion so you never lose track.

Built with Dart, MCP, Google Gemini, and the Notion API.

Getting Started

You’ll need Dart (>= 3.7.0) and GitHub CLI installed.

Grab three API keys before you start:

What Where Notes
Notion token notion.so/profile/integrations See steps below
GitHub PAT github.com/settings/tokens Classic token, repo + actions scopes
Gemini key aistudio.google.com/apikey Any project works

You can either follow the steps in the next section, or have a look at the demo video to learn more about the tool and get step-by-step instruction for installing Ghost…

The project has three main pieces:

  • ghost_maintainer_mcp/ — The MCP server (Dart). 5 tools, 2 resources, 2 prompts.
  • notion_setup/ — The CLI tool and GitHub Action scripts that power the automation.
  • .github/workflows/ — 4 workflows: issue ingestion, triage, implementation, and PR archival.

The CLI doubles as a globally installable tool (dart pub global activate from the repo, no publishing needed).

How I Used Notion MCP

Notion is the central nervous system of Ghost Maintainer. Every piece of data flows through it.

As the Issue Tracker:
The Triage Queue, Maintenance Backlog, Feature Backlog, and Archive are all Notion databases. Each has structured properties (Stage, Priority, Labels, AI Summary, PR URL, Issue Number) that the automation reads and writes. When AI triages an issue, it updates the Notion page with its analysis. When a PR is created, the URL gets linked back. When the PR merges, the item moves to Archive with a timestamp.

As the AI’s Memory:
The Project Vision Statement page is a Notion document that the AI reads before every triage and investigation. It contains your project’s mission, principles, and current focus areas. This means the AI doesn’t just categorize issues mechanically — it understands that “security issues are always P0” or “we’re in a stability phase, deprioritize new features.” You edit a Notion page, and the AI’s behavior changes.

Through MCP:
The MCP server exposes Notion data as resources (ghost://vision for the vision statement, ghost://backlog/summary for live stats) and provides tools that read from and write to Notion. When you ask the MCP server to triage an issue, it pulls the vision from Notion, calls Gemini, then writes the results back to Notion. The protocol bridges conversational AI with structured project data.

The flow end to end:

GitHub Issue
    --> GitHub Action
    --> Notion Triage Queue
    --> AI Classification
    --> Notion Backlog
    --> AI Investigation
    --> GitHub PR
    --> Merge
    --> Notion Archive

Every step is visible in Notion. You always know where every issue is, what the AI decided, and why. No black boxes.

What I like most about this setup is that Notion becomes the source of truth that both humans and AI operate on. I check my Notion dashboard, I see what’s happening. The AI checks the same dashboard. We’re looking at the same board.

Full-Link Accountability for AI Agents

Core Event Primitives

Four standard event types (J, D, V, T) cover the full accountability lifecycle:

  • J: Judge – Create and initiate a judgment/decision

  • D: Delegate – Transfer authority or assign a task

  • V: Verify – Review and validate a record

  • T: Terminate – End a judgment or task lifecycle

Pain Points & Technical Solutions

Pain Point 1: Broken chain in multi-agent workflows, unable to trace root cause

Trigger Primitives: J + D

Solution: Add a task_based_on field to every record to enforce a hash reference to the parent task. A null value indicates the start of a chain; a populated value links to a preceding action, ensuring full end-to-end traceability.

Pain Point 2: Unclear accountability, hard to assign fault for errors

Trigger Primitives: D + V

Solution: Permanently include a who field in every record, bound to the actor’s DID or public key hash. Combined with cryptographic signing, records become non-repudiable and tamper-proof, enabling precise accountability.

Pain Point 3: Lack of compliant audit evidence for regulatory requirements

Trigger Primitives: V + T

Solution: Equip every record with a timestamp, unique nonce, and signature verification. Full audit trails with replay protection are natively supported, directly satisfying compliance requirements under the EU AI Act and Singapore IMDA frameworks.

Core Data Structure (Ready for Use)

{
“jep”: “1”,
“verb”: “J”,
“who”: “did:example:agent-789”,
“when”: 1742345678,
“what”: “122059e8878aa9a38f4d123456789abcdef01234”,
“nonce”: “f47ac10b-58cc-4372-a567-0e02b2c3d479”,
“aud”: “https://platform.example.com“,
“task_based_on”: “hash-of-parent-task”,
“ref”: “”,
“sig”: “eyJhbGciOiJFZERTQSJ9…”
}

Key Field Definitions

  • verb: Required; one of [J, D, V, T]

  • who: Required; unique identifier of the actor

  • when: Required; Unix timestamp to prevent stale/tampered records

  • nonce: Required; UUIDv4 to prevent replay attacks

  • task_based_on: Traceability field; hash of the parent task

  • ref: For verification events only; references the ID of the event being checked

  • sig: Required; JWS digital signature

Verification Logic (Pseudocode)

def verify_record(record):
# 1. Verify signature integrity
if not verify_jws_signature(record):
return “INVALID”
# 2. Ensure nonce uniqueness to prevent replay
if not is_valid_nonce(record[“nonce”]):
return “INVALID”
# 3. Validate timestamp within acceptable window
if not is_within_time_window(record[“when”]):
return “INVALID”
# 4. Verify parent task chain integrity
if record.get(“task_based_on”) and not task_exist(record[“task_based_on”]):
return “INVALID”
# 5. Verify events must include a reference
if record[“verb”] == “V” and not record.get(“ref”):
return “INVALID”
return “VALID”

Critical Security Rules

  • All records must be signed; any tampering invalidates the signature

  • Nonces must be globally unique; duplicate requests are rejected

  • Timestamp tolerance: ±5 minutes to account for clock skew

  • Verify (V) events must include a ref field to avoid circular validation

  • Ed25519 recommended; support for SM2, ECDSA P-256, and post-quantum algorithms

Optional Extensions

  • Task State: Add status field (pending, executing, completed, terminated)

  • Assignment Log: Record DID of assigner and assignee

  • Result Validation: Include confidence score and human review flag

  • Fault Handling: Log missing parent tasks and failure reasons for chain breaks

Implementation based on these IETF Drafts:

  • draft-wang-jep-judgment-event-protocol-01

  • draft-wang-jac-00

Building a Hate Speech Dataset with Responsible Web Scraping

Why Build Hate Speech Datasets?

AI moderation models are only as good as their training data. Researchers and companies building content moderation systems need labeled datasets of harmful content. Building these datasets responsibly requires careful ethical consideration and technical skill.

Ethical Framework First

Before writing any code, establish guidelines:

  • Purpose limitation — data used only for building detection models
  • Minimization — collect only what is needed for training
  • No amplification — never republish or redistribute raw hate speech
  • IRB approval — get institutional review board clearance for academic work
  • Secure storage — encrypt datasets, limit access

Architecture

Scraper -> Anonymizer -> Labeler -> Encrypted Storage

Setup

pip install requests beautifulsoup4 pandas cryptography

For accessing forums at scale, ScraperAPI handles proxy rotation and rate limiting.

The Responsible Scraper

import requests
from bs4 import BeautifulSoup
import pandas as pd
import hashlib
from datetime import datetime

SCRAPER_API_KEY = "YOUR_KEY"

def scrape_forum_posts(forum_url, max_pages=5):
    posts = []
    for page in range(1, max_pages + 1):
        url = f"http://api.scraperapi.com?api_key={SCRAPER_API_KEY}&url={forum_url}?page={page}"
        response = requests.get(url)
        soup = BeautifulSoup(response.text, "html.parser")

        for post in soup.select(".post, .comment, .message"):
            content = post.select_one(".content, .body")
            if content:
                posts.append({
                    "text": content.text.strip(),
                    "source_hash": hashlib.sha256(forum_url.encode()).hexdigest()[:12],
                    "scraped_at": datetime.now().isoformat()
                })
    return posts

Anonymization Pipeline

Critical step: remove all personally identifiable information before labeling.

import re

def anonymize_text(text):
    # Remove usernames, emails, URLs
    text = re.sub(r"@w+", "[USER]", text)
    text = re.sub(r"[w.+-]+@[w-]+.[w.]+", "[EMAIL]", text)
    text = re.sub(r"https?://S+", "[URL]", text)
    # Remove phone numbers
    text = re.sub(r"bd{3}[-.]?d{3}[-.]?d{4}b", "[PHONE]", text)
    # Remove names (basic NER approach)
    text = re.sub(r"b[A-Z][a-z]+ [A-Z][a-z]+b", "[NAME]", text)
    return text

def process_posts(raw_posts):
    processed = []
    for post in raw_posts:
        processed.append({
            "text": anonymize_text(post["text"]),
            "source_hash": post["source_hash"],
            "scraped_at": post["scraped_at"],
            "anonymized": True
        })
    return processed

Labeling Framework

LABEL_SCHEMA = {
    0: "clean",
    1: "offensive_language",
    2: "hate_speech",
    3: "threat"
}

def create_labeling_batch(posts, batch_size=50):
    df = pd.DataFrame(posts)
    df["label"] = None
    df["annotator"] = None
    df["confidence"] = None

    batches = [df[i:i+batch_size] for i in range(0, len(df), batch_size)]

    for i, batch in enumerate(batches):
        batch.to_csv(f"batch_{i:03d}.csv", index=False)
        print(f"Created batch_{i:03d}.csv with {len(batch)} samples")

    return len(batches)

Encrypted Storage

from cryptography.fernet import Fernet
import json

def encrypt_dataset(data, key_file="dataset.key"):
    key = Fernet.generate_key()
    with open(key_file, "wb") as f:
        f.write(key)

    cipher = Fernet(key)
    json_data = json.dumps(data).encode()
    encrypted = cipher.encrypt(json_data)

    with open("dataset.enc", "wb") as f:
        f.write(encrypted)

    print(f"Dataset encrypted. Key saved to {key_file}")
    print("Store the key separately from the dataset!")

def decrypt_dataset(key_file="dataset.key"):
    with open(key_file, "rb") as f:
        key = f.read()
    cipher = Fernet(key)
    with open("dataset.enc", "rb") as f:
        encrypted = f.read()
    return json.loads(cipher.decrypt(encrypted))

Quality Metrics

def calculate_inter_annotator_agreement(labels_a, labels_b):
    agreement = sum(a == b for a, b in zip(labels_a, labels_b))
    return agreement / len(labels_a)

def dataset_statistics(df):
    print(f"Total samples: {len(df)}")
    print(f"Label distribution:")
    for code, name in LABEL_SCHEMA.items():
        count = (df["label"] == code).sum()
        print(f"  {name}: {count} ({count/len(df)*100:.1f}%)")

Handling Protected Sources

Some forums require authentication or have aggressive bot detection. Use ThorData residential proxies for access, and ScrapeOps to monitor success rates.

Best Practices

  1. Never scrape private conversations or DMs
  2. Get ethics board approval for academic research
  3. Use content warnings when sharing results
  4. Balance your dataset to avoid bias
  5. Document your methodology thoroughly

Conclusion

Building hate speech datasets is essential for AI safety but must be done responsibly. With tools like ScraperAPI, proper anonymization, and encrypted storage, you can create valuable training data while respecting privacy and ethics.