Articles

GitHub API: We Built a Bot. Then We Replaced It with an Agent.

Ibby SyedIbby Syed, Founder, Cotera
9 min readMarch 8, 2026

GitHub API: We Built a Bot. Then We Replaced It with an Agent.

GitHub API: We Built a Bot. Then We Replaced It with an Agent.

Tomás wanted to stop babysitting dependency updates across our 14 repos. Dependabot was doing its job -- opening PRs -- but roughly 40% of them blew up in CI. Lockfile format mismatches, linting failures, the usual mess that needs a human to untangle. His pitch: build a bot that watches for Dependabot PRs, clones the repo, applies our formatting rules, regenerates the lockfile, pushes, and marks the PR ready for review. One week, he said. It took three.

The bot worked well for six months. Then it became my least favorite line item in our engineering time tracking. Here's the full story, starting with the API itself, because understanding the GitHub API is useful whether you build a bot or not.

The GitHub REST API

GitHub's REST API is big and, to its credit, well-documented. Over 300 endpoints spanning repos, issues, PRs, commits, branches, releases, orgs, teams, actions, packages -- the list keeps going. Everything lives under https://api.github.com, responses are JSON, and if you've hit any modern REST API before, you'll feel at home within minutes.

Authentication is your first fork in the road. Personal Access Tokens are the fast path: generate one in GitHub settings, drop it into the Authorization header as a Bearer token, done. Fine-grained PATs are worth the extra minute -- they let you lock the token down to specific repos and specific permissions (read contents, write PRs, admin branch protection). Classic PATs give you broader access with less hassle, but they're also less secure. For a bot operating on your own repos, fine-grained PATs are the obvious choice.

GitHub Apps are the heavier-duty option. You install an App on an org or specific repos, it authenticates with a private key to get a short-lived installation token, and then it uses that token for API calls. The upside: granular permissions, and the bot acts as itself instead of impersonating a user. The downside: real setup overhead. You register the app, generate a private key, implement the JWT-based auth flow, and handle token refresh every hour when they expire. Tomás migrated from a PAT to a GitHub App in week two -- he was tired of the bot's commits showing up under his name in the git log.

Rate limiting allows 5,000 requests per hour for authenticated requests. That sounds generous until you calculate the actual volume. For a single Dependabot fix across one repo, the bot made roughly eight API calls. With 30 to 40 Dependabot PRs per week and polling every 15 minutes, Tomás needed to add backoff logic when approaching the threshold.

Pagination is everywhere, and I mean everywhere. List pull requests? 30 per page by default, 100 max if you set per_page. Got a repo with a lot of open PRs? Follow the Link header for the next page. Directory listings? Paginated. Every single endpoint that returns a list? Paginated. Tomás wrote a generic pagination helper on day two -- it chased Link headers and accumulated results -- and that little function ended up getting called by nearly every other piece of the bot.

Webhooks replaced polling as the bot's trigger mechanism. Tomás started by polling every 15 minutes, which felt wasteful, so he wired up a webhook on each repo that fired on PR events. Dependabot opens a PR, GitHub POSTs the payload to the bot, bot checks if it's a Dependabot PR, and if so, kicks off the fix-and-push flow. Much cleaner.

The webhook setup itself, though, was its own project. You need a publicly accessible endpoint (ngrok during dev, a real endpoint in staging), an HMAC secret for verifying payloads, and you have to configure all of this on each of the 14 repos individually.

The Bot: Three Weeks of Building

Week one was authentication, API helpers, and the basic flow. Authenticate as a GitHub App, list open PRs, filter for Dependabot PRs, get the diff, identify the files that needed fixing. The core logic was about 400 lines of TypeScript.

Week two was where complexity started compounding. The fix logic sounds simple when you describe it: check out the PR branch, run the linter, regenerate the lockfile, commit, push. In practice, each of our 14 repos uses a slightly different linting config. The bot had to sniff out which config was active and run the corresponding commands. Tomás ended up writing detection logic for four different linter setups. That fix layer alone came to 600 lines.

Week three was the part nobody warns you about: error handling, retry logic, deployment plumbing. What do you do when the API hands back a 502? When a webhook payload shows up garbled? When the linter explodes on syntax errors that Dependabot itself introduced? When your push races against someone else's push to the same branch? Every edge case needed its own code path. The retry logic alone -- exponential backoff, max attempt caps, dead-letter logging for permanently broken operations -- ran to 150 lines.

Final tally: roughly 1,800 lines of TypeScript, running in a Docker container, with a webhook receiver, a job queue for processing PRs, and a SQLite database tracking which PRs had already been handled.

Six Months of Maintenance

Month one was quiet. The bot processed about 35 Dependabot PRs per week with a success rate of about 88%. The 12% failure rate was mostly edge cases in the fix logic: unusual dependency configurations, monorepo package structures the bot didn't handle, and one case where Dependabot bumped a dependency that required a Node.js version upgrade, which the bot's fix logic couldn't address.

Month two brought the first API change. GitHub updated the pull request review endpoint's response format. A field the bot relied on to check if a PR had already been reviewed changed from a string to an object. The bot crashed on every processed PR until Tomás noticed and pushed a fix. Total downtime: about six hours. Fix time: 45 minutes.

Month three was a fun one. The GitHub App's private key expired. Normally these last a year, but Tomás had generated his during early testing with a shorter expiration and forgot about it. The bot just... stopped working. Silently. The 401 errors looked like temporary hiccups to the retry logic, so it kept hammering away for two straight days before anyone checked the logs. Tomás regenerated the key, redeployed, and -- lesson learned -- added an alert specifically for auth failures.

Month four, we added two new repos. Each one needed the webhook configured, the linter detection logic updated (one repo used a linting config the bot hadn't seen before), and the CI configuration mapped. Tomás spent about four hours adding support for the new repos.

Month five, the webhook endpoint went down during a deployment. The bot's container took three minutes to restart, and two Dependabot PRs were missed. Tomás added a reconciliation job that polled the API hourly to catch missed webhooks. Another 200 lines, another cron schedule.

Add it all up: 22 hours of maintenance across six months, roughly 3.7 hours every month. On top of the three weeks it took to build the thing in the first place.

The Switch

The dependency version bumper agent replaced the entire bot in an afternoon. It logs into GitHub, reads Dependabot PRs, figures out what needs fixing, applies linting and formatting rules specific to each project, and pushes clean commits. Same workflow, same outcomes -- minus the 1,800 lines of custom code, the Docker container, the job queue, the SQLite database, the webhook plumbing, and the monthly maintenance tax.

First-month success rate: 91%. That's on par with the bot's 88% -- which took six months of tuning to reach. The 9% the agent couldn't handle? Same stuff the bot couldn't handle: dependency bumps that need actual architectural changes or Node.js version upgrades. Those will always need a human.

But the real win is operational. The bot needed monitoring, deployment babysitting, key rotation, webhook upkeep, and code patches every time GitHub tweaked an API response. The agent needs none of that. Under the hood, it makes the same GitHub API calls. The difference is that auth, pagination, rate limiting, and error handling are the platform's problem now -- not something Tomás debugs at 10 PM on a Tuesday.

Rafael inherited the bot when Tomás moved to another project, and he was probably the happiest person on the team when we made the switch. "Two weeks just learning how the thing worked. A third week fixing a retry logic bug. Then we swapped in the agent and I got all three of those weeks back."

When to Build, When to Use an Agent

If you're a developer considering a custom GitHub integration, here's what six months of bot ownership taught us.

Build a custom integration when GitHub connectivity is something you're selling. If you're shipping a developer tool that end users install, and GitHub integration is a feature on your pricing page, then yes -- build it yourself. You need that level of control, branding, and edge-case coverage. The maintenance hours count as product investment.

But for internal automation? Don't. Three weeks of dev time, 22 hours of maintenance over six months, deployment infrastructure, monitoring dashboards, documentation so the next person can maintain it when the original author inevitably moves teams. Every hour spent on that is an hour not spent on your actual product.

Tomás's retrospective: "The bot was a good engineering project. I learned a lot about GitHub Apps, webhook architecture, and production error handling. I'd do it again as a learning exercise. I wouldn't do it again as a way to solve the actual problem. The agent solves the problem without teaching me anything, which is exactly what I want from internal tooling."


Try These Agents

For people who think busywork is boring

Build your first agent in minutes with no complex engineering, just typing out instructions.