Dependabot, but for APIs. Reads a vendor's change feed, finds the affected code in your repo, writes the fix, opens the pull request — and tells you honestly how sure it is.
Quickstart · How it works · Confidence · Feed format · Internal APIs · Architecture · Contributing
A product of Retia Labs
A vendor ships a breaking change. They write a changelog entry, send an email, and hope. Then every customer independently rediscovers the same migration, at a different time, with a different amount of panic. Some don't discover it at all until production breaks.
The other half of the same problem is quieter: useful features launch and nobody notices, because nobody reads changelogs.
This was an unavoidable cost right up until developers started routinely granting codebase access to agentic tools. That infrastructure now exists and is normal. What's missing is the layer that connects a provider's change to a customer's code.
- Changelog entry → email → hope → incident
+ Change feed → scan → patch → pull request → review → mergeAlpha, and honest about it. This is a working tool with a real test suite and a real precision benchmark — and, so far, no production users. Nothing here is theoretical, but nothing here has survived contact with a stranger's codebase either.
Contributions welcome, especially vendor feeds — coverage is the bottleneck. See CONTRIBUTING.md. A false positive, or an over-confident
autolabel, is the most serious class of bug this project has, and those reports get priority.Licensed Apache-2.0. It was briefly closed and proprietary at v0.6.0; that was a mistake, and NOTICE.md says why rather than hiding it.
No install, no build step, no dependencies. Node ≥ 20.
git clone https://github.com/likalight/apisentry
cd apisentry
node bin/apisentry.mjs run --patchThat runs against the bundled demo app, which uses Stripe, OpenAI, and Twilio — wrongly.
The demo feeds in
fixtures/feeds/are invented and marked"fictional": true— Stripe did not remove the Charges API. They exist because a demo needs a feed that provably exercises all three confidence levels. Feeds drafted from real vendor changelogs live inchanges/.
What you get
The Charges API is removed in favour of PaymentIntents [stripe-2026-06-01-charges-removed]
provider stripe type breaking severity critical fix auto
4 site(s) in 2 file(s) +4 -4
src/lib/stripe.ts:9 return stripe.charges.create({
src/routes/checkout.ts:6 const charge = await stripe.charges.create({
--- a/src/lib/stripe.ts
+++ b/src/lib/stripe.ts
@@ -6,7 +6,7 @@
export async function chargeCustomer(customerId: string, amountCents: number) {
- return stripe.charges.create({
+ return stripe.paymentIntents.create({
Legacy Completions endpoint is removed [openai-2026-04-02-completions-removed]
provider openai type breaking severity critical fix partial
note The call target is rewritten automatically, but `prompt:` has no mechanical
translation — a human has to decide the system/user split.
messages.create now accepts sendAt for scheduled delivery [twilio-...]
provider twilio type feature severity low fix review
scanned 4 files · 2 auto-fixable · 1 partial · 1 review-only
Point it at your own code:
node bin/apisentry.mjs scan --target ~/my-app # what's affected?
node bin/apisentry.mjs plan --target ~/my-app --patch # what would change?
node bin/apisentry.mjs pr --target ~/my-app --open # ship the PRs check ──────► scan ──────► plan ──────► apply ──────► pr
pull feeds find build write branch, commit,
for changes affected patches + working push, gh pr create
you haven't code confidence tree (one PR per change)
seen yet
| command | what it does |
|---|---|
check |
Pull vendor feeds; list changes this repo hasn't been told about yet |
scan |
Run each change's detectors over your codebase; report every affected site |
plan |
Apply migrations in memory; produce patches and a confidence label |
apply |
Write the fixes to the working tree |
pr |
Branch, commit, push, gh pr create |
run |
check + plan in one pass |
explain <id> |
Everything known about one change, including its compiled regexes |
ack <id...> |
Mark a change handled without touching code, so it stops being reported |
| `draft <url | file>` |
keygen / sign |
Generate an ed25519 keypair, and sign a feed with it |
Add --verify to plan, apply, or pr to run the target's test suite against each patch.
Flags: --target --provider --change --all --patch --json --commit --push --open
--update --yes --no-gating --no-rooting --verify[=cmd] --include-drafts
A tool that reports things you don't care about gets uninstalled faster than one that misses things. Before a single detector runs, two questions get answered.
1. Does this repo even use this vendor? apisentry reads the dependency manifests — package.json,
requirements.txt, pyproject.toml, go.mod, Gemfile, composer.json, Cargo.toml, every
workspace of a monorepo — and matches them against the change's appliesTo:
| the repo… | what runs |
|---|---|
| depends on the package, version in range | everything |
| depends on it, version outside the range | nothing — skipped |
| doesn't depend on it | only detectors that don't presuppose an import |
| has no manifest at all | everything — we can't tell, so we don't guess |
That third row is deliberate: someone calling your REST API with raw fetch and no SDK is still
affected by an endpoint removal, and still gets the PR.
2. Does this call actually come from the SDK? symbol and param detectors only count when the
match hangs off an identifier that traces back to the vendor. Imports are resolved, and relative and
alias-path re-exports are followed up to three hops — because everyone wraps their client in
lib/stripe.ts.
import Stripe from 'stripe'
const s = new Stripe(key)
s.charges.create({}) // matched
db.charges.create({}) // suppressed — db has nothing to do with StripeNothing is filtered silently. Every skipped change prints its reason, and suppressed match counts are
always shown; --no-gating and --no-rooting turn either filter off.
A tool with commit access to other people's repositories has to be honest about what it does and doesn't know. After running a change's migrations, apisentry re-runs that change's own detectors on the result. What still matches decides how the PR opens:
| meaning | how the PR opens | |
|---|---|---|
auto |
Every detected usage was rewritten; a re-scan comes back clean | Normal PR |
partial |
Some rewritten, detectors still fire | Draft PR, with the manual: note surfaced |
review |
No migration exists — we can only say where to look | Report / draft only |
That inversion matters: the party publishing a change doesn't get to grade its own completeness.
Detector analysis is a statement about apisentry, not about your codebase. --verify runs your
test suite against the patch and reports the result in the PR:
node bin/apisentry.mjs plan --target ~/my-app --verify The Charges API is removed in favour of PaymentIntents
provider stripe type breaking severity critical fix partial
BROKEN npm test (1.0s)
confidence downgraded to partial — the suite fails with this patch applied
The baseline is established before anything is patched, because a suite that was already red
proves nothing about the migration. Then the patch is applied, the suite re-runs, and the tree is
restored in a finally — leaving a customer's working tree patched would be a worse bug than any
this could catch.
| verdict | meaning |
|---|---|
verified |
green before, green after — evidence, not proof |
broken |
green before, red after → downgraded to partial, PR opens as a draft with the output |
baseline-red |
already failing, so the run proves nothing. No downgrade — an inconclusive result must not masquerade as a verdict |
no-tests / timeout |
unverified, and the PR says so |
Verification can only ever lower a confidence label, never raise one. A green suite means the tests that exist still pass, which is not the same as correct.
It's opt-in because it executes commands from the target repository — see
SECURITY.md. Override the detected command with --verify="pnpm test".
partial is not a failure mode, it's the common case. In the demo, OpenAI's Completions removal
rewrites openai.completions.create → openai.chat.completions.create and choices[0].text →
choices[0].message.content mechanically — but prompt: → messages: has no mechanical
translation, so it ships a note and opens as a draft.
A draft PR saying "I did the easy 80%, here's the 20% only you can decide" is useful. A confident PR that silently broke checkout is the thing that gets this class of tool banned everywhere.
The real artifact here is the file a provider should publish next to their changelog. It's small on purpose — a vendor should be able to write one by hand in ten minutes.
{
"id": "stripe-2026-06-01-charges-removed",
"title": "The Charges API is removed in favour of PaymentIntents",
"appliesTo": { "packages": ["stripe"], "versions": ">=8.0.0" },
"type": "breaking",
"severity": "critical",
"effectiveAt": "2026-09-01",
"url": "https://stripe.com/docs/upgrades#2026-06-01",
"detect": [
{ "kind": "symbol", "value": "charges.create" },
{ "kind": "endpoint", "value": "/v1/charges" }
],
"migrate": [
{ "kind": "rename", "from": "charges.create", "to": "paymentIntents.create" },
{ "kind": "replace", "value": "/v1/charges", "replacement": "/v1/payment_intents" }
]
}Detectors answer "what does affected code look like?"
symbol— a dotted call path, whitespace-tolerant, won't match inside a longer identifierendpoint— a URL path, but only inside a string literal, so comments and prose don't fire. This is what catches rawfetchcallers who never touched your SDK.param— a request key, optionally scoped to a callregex— the escape hatch
Migrations are text substitution only. No code from a feed is ever executed.
"migrate": [] means review-only — which is the correct encoding for a feature announcement, and
solves the "nobody noticed it launched" half of the problem with no autofix at all.
Full reference: docs/feed-format.md
Almost no vendor publishes a machine-readable feed today. That's the chicken-and-egg problem
at the centre of this idea, and draft is the answer to it — point it at a changelog and it
produces a feed entry a human then confirms.
node bin/apisentry.mjs draft https://vendor.example/changelog.rss \
--provider vendor --packages vendor-sdk --out changes/vendor.json 2026-07-14 — The Charges API is removed [acme-2026-07-14-the-charges-api-is-removed]
type breaking severity critical confidence medium
symbol acme.charges.create
endpoint /v1/charges
migrate acme.charges.create → acme.payments.create
manual The argument shape differs; check `amount` handling.
Before this stops being a draft
[ ] Every entry describes a real API change, not a docs or pricing note
[ ] Detectors are named in the changelog, not inferred from the product name
[ ] Detectors do not fire on unrelated code — check with `apisentry scan`
...
Run against the three real SDK release feeds, the offline extractor currently produces 17
Twilio endpoint removals and 1 Stripe deprecation from the last 15 releases of each, and
nothing for OpenAI — whose recent releases contain no breaking changes with code spans. Those
are in changes/, all still marked draft.
It reads RSS, Atom, HTML and Markdown, and works in two modes:
- Heuristic (offline, no key). Segments the changelog, classifies each entry, and pulls candidate detectors out of backticked code spans. It never invents a migration — guessing a rewrite from prose is exactly the confident-and-wrong this project exists to avoid, so every heuristic draft is review-only.
- Model (
ANTHROPIC_API_KEYset). Claude reads the segmented text and fills in the feed schema through a forced tool call, including migrations where the rewrite is genuinely mechanical. It's prompted to return an empty list rather than pad, and to rate its own confidence honestly — alowcosts nothing and a wrongautocosts everything.
Every draft is inert. Entries carry "draft": true, and check lists them separately while
scan, plan and pr skip them entirely until a human removes the flag. An agent wrote it is
not a reason to trust it.
A passing test suite says the code does what I meant. It says nothing about whether the tool is
right about real code. bench/ measures the number that actually matters:
npm run bench repo files findings no-filters expected verdict
──────────────────────────────────────────────────────────────────────
ledgerpilot 77 0 0 0 ok
provenancetrail 86 0 0 0 ok
demo-app 4 10 10 10 ok
167 files scanned · 10 findings · 29 without gating/rooting
the two precision filters removed 19 match(es) (66%)
The feed set includes a collision probe — a fictional vendor whose detectors are deliberately
generic (invoice.create, document.update) and collide head-on with ordinary Prisma calls in
a real application. Stripe genuinely has invoices.create, so this is not a contrived shape.
On a real 77-file Next.js codebase: 19 matches, 0 reported. That is the number the two
precision filters exist for. Add your own repos to bench/repos.json with an
expect count and the run fails when it moves.
The two shapes this could take are the same tool with a different sources list.
| Per-provider agent | Neutral third party |
|---|---|
|
A vendor ships an agent that knows only their API. {
"sources": [
{ "kind": "http",
"url": "https://stripe.com/.well-known/api-changes.json" }
]
} |
Dependabot-for-APIs, tracking many vendors and mirroring the ones with no feed yet. {
"sources": [
{ "kind": "http", "url": ".../stripe/api-changes.json" },
{ "kind": "http", "url": ".../openai/api-changes.json" },
{ "kind": "local", "path": "changes" }
]
} |
A source that 404s or serves garbage degrades that source only — one vendor's broken feed must
never take the run down. Both configs are in examples/config/.
Drop examples/github-action/apisentry.yml into
.github/workflows/. Nightly, it opens PRs for anything new:
- run: node .apisentry-tool/bin/apisentry.mjs pr --target . --open --yes --update
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Not on npm yet, so the workflow clones the repo rather than running
npx apisentry.
Per-repo state lives in .apisentry/state.json (self-gitignoring), so a change already handled here
is never re-reported. Without that, the nightly run reopens the same PR forever and gets muted in a
week.
- Everything defaults to reporting.
pris a dry run unless--commit,--push, or--open.applyprompts before writing.--verifyis opt-in because it executes commands from the target repo. - Path allowlists (
paths.include/paths.exclude) bound which files a migration may touch. - Catastrophic regex patterns are rejected at load, and a detector that proves slow is retired mid-run rather than burning the whole scan.
- Never runs on a dirty working tree. One branch per change, off your current branch.
- No feed code is ever executed — migrations are text substitution. No
eval, no plugin loading. - Zero runtime dependencies, so the supply-chain surface is this repo and Node itself.
- Feeds are trusted input. Only configure sources you'd grant commit access to.
Full threat model, including the known gaps: SECURITY.md
bin/apisentry.mjs CLI entry point
src/schema.mjs feed format: validation + detector/migration compilation
src/feeds/index.mjs local + http feed adapters
src/scan.mjs codebase walker and detector runner
src/plan.mjs migrations → patches → confidence
src/diff.mjs dependency-free unified diff
src/pr.mjs git/gh: branch, commit, push, open
src/state.mjs what this repo has already been told
changes/ demo feeds: stripe, openai, twilio
fixtures/demo-app/ a small codebase that uses all three, wrongly
docs/ feed format + architecture
examples/ CI workflow and both config shapes
npm test # 74 unit + regression tests
node test/assert-demo.mjs # integration: all 4 demo changes classify correctlyCI runs on Node 20/22/24 across Linux, macOS, and Windows.
Honest list, in the order things actually block each other.
Shipped
- Relevance gating (0.2.0) — dependency manifests decide whether a change applies at all.
- Import-aware detection (0.2.0) — wrapped and aliased clients are followed.
- Idempotent PR flow (0.2.0) — reruns skip or refresh instead of failing.
- Verification (0.3.0) —
--verifyruns the target's own suite and downgrades on failure. - Path allowlists and regex safety (0.3.0).
- Changelog → feed extraction (0.4.0) —
apisentry draftproduces a feed for review. - Feed signing (0.7.0) — ed25519; tampering is fatal under every configuration.
- A benchmark that discriminates (0.7.0) — 19 real collisions suppressed, 0 reported.
Open, in priority order
- Nobody has used this. The riskiest assumption in the project is that a developer will merge a bot's PR against their payment code, and it is untested. Everything below is cheaper than finding that out.
- All 18 real feed entries are still
draft. Pointed at its own bundled feeds, apisentry currently proposes nothing. Reviewing and promoting them — or deciding they aren't worth promoting — is the difference between a pipeline and a product. - The model extraction path has never run against the live API. Everything around it is tested; the call itself is not. Needs a key.
- More vendor feeds. Coverage is the real bottleneck, and the reason the extractor exists.
- Full AST detection. Import resolution covers the common cases; a parser would cover dynamic requires, re-exported namespaces, and non-JS languages properly. Tree-sitter behind the existing detector interface.
- True regex preemption. Patterns are linted and slow detectors retired, but a single hostile match can still hang — that needs a worker thread or a different engine.
The engine isn't the moat — it's about three thousand lines of regex, git plumbing and careful error handling. What compounds is the feed corpus (almost no vendor publishes one, and keeping hundreds accurate never finishes), cross-customer signal about which migrations actually land, and being the channel a provider's changes reach customers through.
The wedge is internal APIs: a platform team ships a breaking change and chases twelve consuming teams by Slack. Same product, no vendor cooperation needed, obvious budget owner, and no cold-start problem — the customer writes their own feed.
Reasoning, and the open questions, in GOVERNANCE.md.
Built from the essay Self-Maintaining APIs by Harsha Gaddipati, which argues that the infrastructure for automated code changes already exists — what's missing is the application layer connecting API providers to their customers' codebases. This is that layer, as a working prototype.
Apache-2.0. Copyright 2026 Retia Labs.
Chosen over MIT for the explicit patent grant, which matters if API providers ever engage with the change feed format. The licensing history — including the day this project went proprietary and came back — is in NOTICE.md.
Keywords: api · breaking-changes · api-versioning · deprecation · changelog · codemod · migration · automated-refactoring · dependabot · renovate · developer-tools · devtools · devex · sdk · static-analysis · pull-request-automation · github-actions · ci-cd · agentic-ai · ai-agents · llm-tools · code-migration · api-lifecycle · zero-dependency · nodejs · cli · open-source