Self-hosted Next.js/TypeScript boilerplate: full OAuth 2.1 + PKCE, API key management with zero-downtime rotation, usage-based Stripe billing, and Redis rate limiting. 7 modules, 300+ tests. One-time €79, no revenue share, no platform lock-in. The metering core is free and open source on npm.
a few months ago i started building custom MCP servers on the side. the tool logic took me an afternoon; the auth and billing infrastructure took weeks. after spending four hours reading RFC 9728 at 3 AM, i ended up parking it with a static API key.
i eventually decided to build it properly using Claude Code as a pair. and yes, AI writes code insanely fast now, that’s literally how this project started. but what an LLM draft doesn't do by default is make the right architectural decisions when money is on the line. early on, the AI generated a webhook handler that returned the exact same 400 error for an invalid signature as it did for a temporary DB outage. that meant Stripe never retried the event, and billing updates vanished in silence. finding, testing, and fixing those silent failures is the actual work. generating code is cheap now; the decisions, the tests, and the edge cases someone already paid to discover are not.
you don’t have to take my word for it, though. I extracted and published the core metering logic as a standalone, free MIT package on npm (`mcp-metering`). you can install it, inspect the code, and judge the engineering quality yourself before deciding if the rest of the stack is worth €79. it’s a full proof of quality, not a stripped-down teaser.
the full boilerplate includes full OAuth 2.1 + PKCE, per-user API key management with zero-downtime rotation, usage-based Stripe billing, and Redis rate-limiting. it’s organized into 7 decoupled modules with 300+ tests. one-time payment of €79, full source code, no revenue share, no platform lock-in, and a 7-day refund policy.
would love to hear your feedback on the architecture or any of the documented trade-offs in the README!
Report
The webhook story is the right thing to lead with. I shipped a Stripe integration this month where everything was green in test mode and the first real card 400'd on a currency param test mode never asked for - and separately found receipts failing silently because the send helper never throws and a try/catch was eating the reason. Money bugs don't announce themselves; you find them by walking the path with a real card and reading the logs like a skeptic.
Which is why I'd second Grace's checklist ask, and add one architectural vote: treat metering as a ledger, not a counter. Reserve before the metered call, settle exactly once keyed on the attempt, refund on failure - Gal's retry-dedupe question mostly dissolves when the settle is idempotent by construction. The counter tells you what happened; the ledger makes sure it only happened once.
Publishing mcp-metering free and inspectable is a genuinely good trust move. €79 for the edge cases someone already paid to discover is fair.
@ryan_davis23 "the counter tells you what happened; the ledger makes sure it only happened once" is such a clean way to frame it. that's basically the exact mental model behind mcp-metering: the atomic claim() before execution acts as the reservation, and finalize() is the idempotent settle keyed on the attempt.
the only slight divergence is that i don't do an explicit refund after the fact. because the record stays unbilled until finalize() completes successfully, an event marked as failed simply never gets pushed to the payment sink. same end result, but nothing hits the billing provider until the work is confirmed.
also 100% with you on walking the path with a real card. mock suites and green test modes verify specs, but they completely miss silent helper failures or currency mismatch edge cases like the one you hit.
and thanks for seconding Grace's checklist idea! i'm replying in her thread as well, but having an explicit end-to-end verification checklist before connecting a live server is a great call. really appreciate the feedback and the support on the open-core approach.
@ryan_davis23 hey ryan, loved your framing on the ledger vs counter mental model during the launch.
quick question as i do a post-mortem on the launch: have you ever actually bought a commercial boilerplate or starter kit for backend/infra before, or do you always end up building that layer internally?
Report
The OAuth 2.1 + PKCE half is the part I'd have paid for. We ship an MCP server next to a local CLI binary, and the awkward case was authenticating a client that has no redirect target of its own — we ended up binding a loopback listener on 127.0.0.1 and handing back a http://localhost:<port>/ca... redirect URI. That took longer to get right than the billing did.
Does the boilerplate cover that non-browser client path (loopback or device code), or is it aimed at hosted MCP servers where a normal redirect URI already exists? Also curious what led you to one-time €79 with no revenue share when the metering core is already open source — that's a deliberate-looking choice.
to give you the direct breakdown on the auth side first:
device code flow (RFC 8628): nope, not supported. the token endpoint strictly handles authorization_code and refresh_token, returning a 400 for any other grant.
loopback with dynamic ports: the interactive consent screen is real (server-rendered with explicit Allow/Deny and TOCTOU checks), but right now the redirect_uri validation regex strictly expects https:// or exact http://localhost:port/. so a dynamic listener on 127.0.0.1 with an arbitrary port gets rejected out of the box.
you're actually the second person in two days to ask for dynamic loopback support, so it's moving straight up the OAuth backlog. expanding that whitelist regex is a small surgical fix rather than a redesign.
on the €79 one-time price with no rev share and open-sourcing mcp-metering:
first, taking a revenue cut on a self-hosted codebase felt completely antithetical to the whole premise. if you self-host, you shouldn't pay a platform tax or worry about lock-in.
second, mcp-metering and the boilerplate solve two different needs. the npm package is a standalone primitive for someone who just wants to add metering middleware to an existing setup. the €79 boilerplate is the full integrated stack, which is most useful when you want the complete auth and billing layer rather than just isolating a single piece. it combines OAuth 2.1 + PKCE, API key rotation, Stripe Billing Meters, and Upstash rate-limiting across 7 modules with 300+ tests.
and honestly, open-sourcing the core was a deliberate trust move. it's a lot easier to decide if a €79 repo is worth it when you can inspect the actual code quality and edge-case handling for free beforehand.
Report
@marc_gil1 Thanks for the straight answer on both. One note on the dynamic loopback item, since it might change the scope of the fix: RFC 8252 §7.3 actually puts this on the authorization server as a MUST — it must allow any port to be specified at request time for loopback redirect URIs, since the OS assigns the ephemeral port at runtime and the client can't pre-register it.
So the fix is probably narrower than widening the whitelist: if the host is the literal 127.0.0.1 or [::1], match scheme + host and skip port validation entirely. §8.3 also recommends the IP literals over the `localhost` hostname, since localhost resolution can be redirected locally — worth covering both in the same pass.
And no argument on the pricing model — not taking a cut of a self-hosted codebase is the right call.
treating loopback as a special case (matching scheme + host for 127.0.0.1 and [::1] while skipping port validation entirely) is much cleaner and actually spec-compliant with §7.3, rather than playing regex whack-a-mole with port whitelists.
the §8.3 point on IP literals vs localhost is especially sharp. my current check actually does the exact opposite by expecting localhost and failing IP literals, which exposes it to local DNS resolution overrides.
updating the backlog item from "expand regex" to "proper RFC 8252 loopback handling with IP literals and dynamic ports". really appreciate you taking the time to point out the exact spec sections!
@yoshiaki_sakae hey yoshiaki, thanks again for pointing out the RFC 8252 §7.3 spec details on loopback handling. super helpful context. quick question while i review the launch: since you already hand-rolled that loopback listener for your server, did the boilerplate simply land too late for your specific build, or even if it existed when you started, would you still have built it in-house anyway? curious what the main factor was there.
Report
The zero-downtime API key rotation is the detail that would save the most pain in practice — keys usually get replaced when something breaks, not during planned maintenance, and any rotation downtime cascades into support tickets. The question I would ask before building on this: does the usage-based metering support different pricing tiers per API key, or is billing flat across all keys on the account? That matters when you want to give different API keys to different customer tiers without running separate instances.
@hazy0 to give you the direct answer: right now billing is flat across all keys on an account.
the relationship is structured as User -> Subscription -> Plan, and quota resolution during a metered event happens by userId, not apiKeyId. that means every key issued under a given account shares the exact same billing plan, quota limits, and pricing rates.
if you want to issue different API keys for different customer tiers without running separate instances, that isn't supported out of the box today. supporting that model would require adding an optional planId to the ApiKey schema and updating the plan resolution engine to check for a key-level plan first, with a fallback to the account-level plan. it's a schema update rather than a full architectural rewrite, but it's a real limitation to keep in mind.
really appreciate the nod on zero-downtime key rotation! production breaking during a key replacement is the exact pain point that pattern is meant to eliminate. and your question about per-key tiers is spot on: it's the exact architectural limit someone needs to know before building on top of it.
@hazy0 hey hazy, thanks again for dropping by the Product Hunt thread.
i'm doing a quick post-launch audit to figure out which missing features are actual dealbreakers vs nice-to-haves. was the missing per-API-key pricing tiers the single blocker keeping you from picking this up, or were there other missing pieces for your setup?
Report
The Stripe webhook example is a strong trust signal. For MCP servers, billing is one of those areas where AI-generated code can look complete while the edge cases quietly decide whether the product is usable.
One onboarding thing I’d want as a small builder: a checklist or test mode that proves the money path end to end — auth, metering, retry behavior, failed payments, and key rotation — before I connect a real server.
@grace_lee26 your checklist idea is completely spot on, and ryan actually seconded it in his thread earlier. that exact sequence (auth, metering, retry behavior, failed payments, key rotation) is the exact path where bugs quietly sit without throwing loud errors.
to be totally clear on what exists today: there's a startup setup-check script in the repository that validates environment variables and tests live connections to both Redis and Postgres before the server boots. but that's just the basic infrastructure sanity check - it doesn't walk the full money path or simulate a failed Stripe webhook end-to-end.
an explicit end-to-end verification checklist or dry-run mode for the payment path doesn't exist yet. taking this as a solid signal though, especially with ryan backing it up, so i'm logging it as an onboarding priority. appreciate the sharp feedback!
@grace_lee26 hey grace, thanks again for the feedback on Product Hunt!
quick follow-up as i prioritize what to build next: was that missing end-to-end money path checklist the main thing holding you back from using something like this, or just one of several blockers?
Report
The OAuth half of this is the part I would pay for before the billing half. I connect MCP servers to my Claude setup as a user most weeks, and the pattern from that side is blunt, a server whose auth works on the first try gets used the same day, and one that fails sits unauthenticated for weeks. I have one in that exact state right now. Does your OAuth 2.1 flow cover the interactive consent dance the desktop AI clients run, or is it aimed at headless API consumers with keys?
it covers the real interactive consent flow, not an auto-approve shortcut. when a client kicks off the OAuth 2.1 PKCE authorization code grant, the user lands on an actual consent page in the dashboard to review requested scopes and click "Allow" or "Deny" (backed by TOCTOU protection in the server action and explicit tests for both decisions). so it's definitely built for interactive consent rather than just headless API keys.
that said, to be 100% transparent about a current gap that might affect your exact setup: right now the redirect_uri validation relies on a web-app whitelist checking for https:// or standard http://localhost:port/. if the desktop client you're using relies on RFC 8252 native app patterns like loopbacks with dynamic ports or custom schemes (like claude://), my current validation regex won't cover it — it's a narrow whitelist, easy to extend but not built in yet.
i also haven't tested it end-to-end against a live Claude Desktop client instance yet—the test suite exercises the server actions and handlers directly with mock clients.
curious though, what's the auth setup or custom scheme on the server sitting unauthenticated in your setup right now? would love to know if it's hitting that exact loopback/scheme restriction so i can prioritize it.
Report
@marc_gil1 Honest answer, the stalled one is a hosted server that authenticates through the Claude client itself, browser consent and then a redirect back to a localhost callback the client spins up on a random port. So it is exactly your RFC 8252 case, loopback with a dynamic port, and a fixed localhost whitelist would refuse it. The stall on my side was friction rather than rejection, the flow needs the human at the keyboard at the right moment and that moment kept losing. If desktop AI clients are buyers you care about, the dynamic port loopback looks like the piece to prioritize, and a live end to end run against a real Claude client would tell you more than the mock suite there.
@abdullah_javaid3 that's brilliant context, thanks a lot for confirming.
on the loopback validation: you're 100% right. expanding the redirect_uri check to validate standard RFC 8252 dynamic ports instead of exact string matching is a surgical fix (adjusting the URI regex / port validator), and i'm adding that directly to the top of the OAuth backlog.
on the friction point: to be totally clear on how it behaves right now, the consent screen itself actually doesn't time out while sitting open. the request parameters live in the form inputs, and clicking "Allow" re-validates them fresh against Postgres with TOCTOU protection. the 60-second TTL only starts after you click "Allow", which is when the single-use authorization code gets issued for the client to exchange.
so while a timeout isn't kicking the user off the page, you nailed the real UX gap: there's zero client-server orchestration built for when the human isn't sitting at the keyboard at that exact moment. no push notification, no deep-linking polling to wake up the client, nothing. the boilerplate handles the auth primitives, but not the "notify the user when it's time to approve" UX.
and you're completely right about the end-to-end testing against a real Claude Desktop instance. headless mock suites verify the protocol specs, but they completely miss this kind of real-world human-in-the-loop friction. really appreciate you breaking this down!
@abdullah_javaid3 hey abdullah, coming back to this because your case is honestly the one i keep thinking about. you had a real server stuck unauthenticated from the exact gap we talked about, not a hypothetical.
i'm doing a brutally honest review of the launch before deciding what to build next. genuine question, no pitch: if the dynamic loopback fix had already been in place when we talked, would you have actually paid for the boilerplate to unblock that server? or was the OAuth piece specifically something you'd want standalone, separate from the full billing stack? trying to understand if the real gap was that one missing feature, or something about how this is packaged.
Report
the webhook silent-failure story in your maker comment is the one that would actually keep me up at night, not the OAuth stuff everyone's asking about. on the usage-based billing side specifically: if Stripe retries a webhook (which it does on any non-2xx, including your own transient DB hiccups), does mcp-metering dedupe on the event id before incrementing usage, or is double-counting on retry something the integrator has to guard against themselves? that's the kind of bug that doesn't throw an error, it just quietly overcharges someone until they notice their invoice looks wrong
@galdayan you're asking the exact right question. double-counting on a retry is the absolute worst kind of bug because everything returns a 200 OK and nobody notices until an invoice looks wrong.
to be totally accurate about where that responsibility lives, there are two distinct surfaces here (one in the standalone mcp-metering package, and one in the full boilerplate):
1. The usage event side (mcp-metering) this is handled via an idempotency key per tool execution, not Stripe's event ID (since the metering package is payment-provider agnostic). same key sent twice = registered exactly once.
the deduplication is guaranteed at the database level, not in app code: an atomic claim() query (INSERT ... ON CONFLICT) happens before executing the callback. it actually uses an ON CONFLICT DO UPDATE scoped to a TTL threshold for pending records, so if a server crashes midway between claiming and settling, the key doesn't turn into a permanent poison pill that blocks future retries.
also, a detail you'll appreciate: the billed: true state is never set before the sink confirms. the strict sequence is finalize(billed: false) → onBillable(event) → markBilled. if your Stripe sync or network call fails midway, the record stays unbilled so you can safely retry it, rather than lying to your DB that it was billed when it wasn't. you can inspect this directly in the npm package code (mcp-metering).
2. The Stripe webhooks side (in the boilerplate) here, yes, there is deduplication by Stripe's event.id via a StripeWebhookEvent table with a hard UNIQUE constraint.
the subtle trick here is that the deduplication marker is written after the handler finishes successfully, not before. if you write it first, a DB crash midway leaves the event marked as processed without actually applying the state change, silent data loss.
we also fixed that webhook bug i mentioned: the handler now strictly splits 400 (invalid signature, permanent failure, don't retry) from 500 (transient DB/network error, Stripe must retry).
the limitation still open (known trade-off): to be 100% transparent with you, there's still a known edge case here. while retries are safe because handlers are idempotent overwrites, Stripe does not guarantee strict delivery order across different events. if an older event gets retried and lands after a newer event was already applied, it could theoretically overwrite the newer state. fixing this with an event.created timestamp check or a manual re-fetch from Stripe is logged in the backlog. in practice the risk window is small at low webhook volume, but it's a real gap and it's documented as such.
really sharp question, this is the exact engineering paranoia that makes billing hard!
Report
@marc_gil1 that event.created ordering gap is the part that would worry me most honestly, more than the retry/dedup stuff which you've clearly thought through carefully. low volume hides it but it doesn't remove it - a webhook queue that gets briefly backed up on your end (a deploy, a cold start) is exactly when out of order delivery gets more likely, not less. do you have a rough sense of how you'd detect it happened after the fact, even before you build the fix? like is there a cheap audit query that would catch a state overwrite from an older event, or would it currently just look like normal data
@galdayan honest answer: no, you couldn't detect it today, and it would look like completely normal data.
here is why: the webhook event table only stores event.id as the PK, the type, and processedAt (when my server handled it, not Stripe's event.created). meanwhile, the target resource (like Subscription) only has Prisma's automatic updatedAt. there's no reference to which event wrote the state or what its original timestamp was. so two events processed sequentially in processedAt order could be completely inverted in event.created order, and no query could catch that after the fact.
also, you nailed the real trigger: a deploy or cold start where webhooks queue up and drain in a batch is the scenario where out-of-order delivery actually lands, far more than steady stream traffic.
storing event.created on the target resource fixes both problems at once - it lets you compare timestamps before applying a write and gives you the audit trail to check afterwards.
your question honestly shifted how i view this backlog item: it's not just a rare edge case, it's an uninstrumented one. moving it up.
@galdayan hey gal, thanks again for the sharp exchange on Product Hunt last week. your point about the uninstrumented event.created gap completely shifted how i prioritized that backlog item.
i'm reviewing all the launch feedback to understand where the real friction is. as someone who evaluated the technical design deeply: was there anything specific you saw in the stack (including the gap you uncovered) that made it an immediate "no", or do you simply not have the problem this solves right now?
MCP-Billing
The webhook story is the right thing to lead with. I shipped a Stripe integration this month where everything was green in test mode and the first real card 400'd on a currency param test mode never asked for - and separately found receipts failing silently because the send helper never throws and a try/catch was eating the reason. Money bugs don't announce themselves; you find them by walking the path with a real card and reading the logs like a skeptic.
Which is why I'd second Grace's checklist ask, and add one architectural vote: treat metering as a ledger, not a counter. Reserve before the metered call, settle exactly once keyed on the attempt, refund on failure - Gal's retry-dedupe question mostly dissolves when the settle is idempotent by construction. The counter tells you what happened; the ledger makes sure it only happened once.
Publishing mcp-metering free and inspectable is a genuinely good trust move. €79 for the edge cases someone already paid to discover is fair.
MCP-Billing
@ryan_davis23 "the counter tells you what happened; the ledger makes sure it only happened once" is such a clean way to frame it. that's basically the exact mental model behind mcp-metering: the atomic claim() before execution acts as the reservation, and finalize() is the idempotent settle keyed on the attempt.
the only slight divergence is that i don't do an explicit refund after the fact. because the record stays unbilled until finalize() completes successfully, an event marked as failed simply never gets pushed to the payment sink. same end result, but nothing hits the billing provider until the work is confirmed.
also 100% with you on walking the path with a real card. mock suites and green test modes verify specs, but they completely miss silent helper failures or currency mismatch edge cases like the one you hit.
and thanks for seconding Grace's checklist idea! i'm replying in her thread as well, but having an explicit end-to-end verification checklist before connecting a live server is a great call. really appreciate the feedback and the support on the open-core approach.
MCP-Billing
@ryan_davis23 hey ryan, loved your framing on the ledger vs counter mental model during the launch.
quick question as i do a post-mortem on the launch: have you ever actually bought a commercial boilerplate or starter kit for backend/infra before, or do you always end up building that layer internally?
The OAuth 2.1 + PKCE half is the part I'd have paid for. We ship an MCP server next to a local CLI binary, and the awkward case was authenticating a client that has no redirect target of its own — we ended up binding a loopback listener on 127.0.0.1 and handing back a http://localhost:<port>/ca... redirect URI. That took longer to get right than the billing did.
Does the boilerplate cover that non-browser client path (loopback or device code), or is it aimed at hosted MCP servers where a normal redirect URI already exists? Also curious what led you to one-time €79 with no revenue share when the metering core is already open source — that's a deliberate-looking choice.
MCP-Billing
@yoshiaki_sakae
to give you the direct breakdown on the auth side first:
device code flow (RFC 8628): nope, not supported. the token endpoint strictly handles authorization_code and refresh_token, returning a 400 for any other grant.
loopback with dynamic ports: the interactive consent screen is real (server-rendered with explicit Allow/Deny and TOCTOU checks), but right now the redirect_uri validation regex strictly expects https:// or exact http://localhost:port/. so a dynamic listener on 127.0.0.1 with an arbitrary port gets rejected out of the box.
you're actually the second person in two days to ask for dynamic loopback support, so it's moving straight up the OAuth backlog. expanding that whitelist regex is a small surgical fix rather than a redesign.
on the €79 one-time price with no rev share and open-sourcing mcp-metering:
first, taking a revenue cut on a self-hosted codebase felt completely antithetical to the whole premise. if you self-host, you shouldn't pay a platform tax or worry about lock-in.
second, mcp-metering and the boilerplate solve two different needs. the npm package is a standalone primitive for someone who just wants to add metering middleware to an existing setup. the €79 boilerplate is the full integrated stack, which is most useful when you want the complete auth and billing layer rather than just isolating a single piece. it combines OAuth 2.1 + PKCE, API key rotation, Stripe Billing Meters, and Upstash rate-limiting across 7 modules with 300+ tests.
and honestly, open-sourcing the core was a deliberate trust move. it's a lot easier to decide if a €79 repo is worth it when you can inspect the actual code quality and edge-case handling for free beforehand.
@marc_gil1 Thanks for the straight answer on both.
One note on the dynamic loopback item, since it might change the scope of the fix: RFC 8252 §7.3 actually puts this on the authorization server as a MUST — it must allow any port to be specified at request time for loopback redirect URIs, since the OS assigns the ephemeral port at runtime and the client can't pre-register it.
So the fix is probably narrower than widening the whitelist: if the host is the literal 127.0.0.1 or [::1], match scheme + host and skip port validation entirely. §8.3 also recommends the IP literals over the `localhost` hostname, since localhost resolution can be redirected locally — worth covering both in the same pass.
And no argument on the pricing model — not taking a cut of a self-hosted codebase is the right call.
MCP-Billing
@yoshiaki_sakae you're completely right on both points.
treating loopback as a special case (matching scheme + host for 127.0.0.1 and [::1] while skipping port validation entirely) is much cleaner and actually spec-compliant with §7.3, rather than playing regex whack-a-mole with port whitelists.
the §8.3 point on IP literals vs localhost is especially sharp. my current check actually does the exact opposite by expecting localhost and failing IP literals, which exposes it to local DNS resolution overrides.
updating the backlog item from "expand regex" to "proper RFC 8252 loopback handling with IP literals and dynamic ports". really appreciate you taking the time to point out the exact spec sections!
MCP-Billing
@yoshiaki_sakae hey yoshiaki, thanks again for pointing out the RFC 8252 §7.3 spec details on loopback handling. super helpful context. quick question while i review the launch: since you already hand-rolled that loopback listener for your server, did the boilerplate simply land too late for your specific build, or even if it existed when you started, would you still have built it in-house anyway? curious what the main factor was there.
The zero-downtime API key rotation is the detail that would save the most pain in practice — keys usually get replaced when something breaks, not during planned maintenance, and any rotation downtime cascades into support tickets. The question I would ask before building on this: does the usage-based metering support different pricing tiers per API key, or is billing flat across all keys on the account? That matters when you want to give different API keys to different customer tiers without running separate instances.
MCP-Billing
@hazy0 to give you the direct answer: right now billing is flat across all keys on an account.
the relationship is structured as User -> Subscription -> Plan, and quota resolution during a metered event happens by userId, not apiKeyId. that means every key issued under a given account shares the exact same billing plan, quota limits, and pricing rates.
if you want to issue different API keys for different customer tiers without running separate instances, that isn't supported out of the box today. supporting that model would require adding an optional planId to the ApiKey schema and updating the plan resolution engine to check for a key-level plan first, with a fallback to the account-level plan. it's a schema update rather than a full architectural rewrite, but it's a real limitation to keep in mind.
really appreciate the nod on zero-downtime key rotation! production breaking during a key replacement is the exact pain point that pattern is meant to eliminate. and your question about per-key tiers is spot on: it's the exact architectural limit someone needs to know before building on top of it.
MCP-Billing
@hazy0 hey hazy, thanks again for dropping by the Product Hunt thread.
i'm doing a quick post-launch audit to figure out which missing features are actual dealbreakers vs nice-to-haves. was the missing per-API-key pricing tiers the single blocker keeping you from picking this up, or were there other missing pieces for your setup?
The Stripe webhook example is a strong trust signal. For MCP servers, billing is one of those areas where AI-generated code can look complete while the edge cases quietly decide whether the product is usable.
One onboarding thing I’d want as a small builder: a checklist or test mode that proves the money path end to end — auth, metering, retry behavior, failed payments, and key rotation — before I connect a real server.
MCP-Billing
@grace_lee26 your checklist idea is completely spot on, and ryan actually seconded it in his thread earlier. that exact sequence (auth, metering, retry behavior, failed payments, key rotation) is the exact path where bugs quietly sit without throwing loud errors.
to be totally clear on what exists today: there's a startup setup-check script in the repository that validates environment variables and tests live connections to both Redis and Postgres before the server boots. but that's just the basic infrastructure sanity check - it doesn't walk the full money path or simulate a failed Stripe webhook end-to-end.
an explicit end-to-end verification checklist or dry-run mode for the payment path doesn't exist yet. taking this as a solid signal though, especially with ryan backing it up, so i'm logging it as an onboarding priority. appreciate the sharp feedback!
MCP-Billing
@grace_lee26 hey grace, thanks again for the feedback on Product Hunt!
quick follow-up as i prioritize what to build next: was that missing end-to-end money path checklist the main thing holding you back from using something like this, or just one of several blockers?
The OAuth half of this is the part I would pay for before the billing half. I connect MCP servers to my Claude setup as a user most weeks, and the pattern from that side is blunt, a server whose auth works on the first try gets used the same day, and one that fails sits unauthenticated for weeks. I have one in that exact state right now. Does your OAuth 2.1 flow cover the interactive consent dance the desktop AI clients run, or is it aimed at headless API consumers with keys?
MCP-Billing
@abdullah_javaid3
it covers the real interactive consent flow, not an auto-approve shortcut. when a client kicks off the OAuth 2.1 PKCE authorization code grant, the user lands on an actual consent page in the dashboard to review requested scopes and click "Allow" or "Deny" (backed by TOCTOU protection in the server action and explicit tests for both decisions). so it's definitely built for interactive consent rather than just headless API keys.
that said, to be 100% transparent about a current gap that might affect your exact setup: right now the redirect_uri validation relies on a web-app whitelist checking for https:// or standard http://localhost:port/. if the desktop client you're using relies on RFC 8252 native app patterns like loopbacks with dynamic ports or custom schemes (like claude://), my current validation regex won't cover it — it's a narrow whitelist, easy to extend but not built in yet.
i also haven't tested it end-to-end against a live Claude Desktop client instance yet—the test suite exercises the server actions and handlers directly with mock clients.
curious though, what's the auth setup or custom scheme on the server sitting unauthenticated in your setup right now? would love to know if it's hitting that exact loopback/scheme restriction so i can prioritize it.
@marc_gil1 Honest answer, the stalled one is a hosted server that authenticates through the Claude client itself, browser consent and then a redirect back to a localhost callback the client spins up on a random port. So it is exactly your RFC 8252 case, loopback with a dynamic port, and a fixed localhost whitelist would refuse it. The stall on my side was friction rather than rejection, the flow needs the human at the keyboard at the right moment and that moment kept losing. If desktop AI clients are buyers you care about, the dynamic port loopback looks like the piece to prioritize, and a live end to end run against a real Claude client would tell you more than the mock suite there.
MCP-Billing
@abdullah_javaid3 that's brilliant context, thanks a lot for confirming.
on the loopback validation: you're 100% right. expanding the redirect_uri check to validate standard RFC 8252 dynamic ports instead of exact string matching is a surgical fix (adjusting the URI regex / port validator), and i'm adding that directly to the top of the OAuth backlog.
on the friction point: to be totally clear on how it behaves right now, the consent screen itself actually doesn't time out while sitting open. the request parameters live in the form inputs, and clicking "Allow" re-validates them fresh against Postgres with TOCTOU protection. the 60-second TTL only starts after you click "Allow", which is when the single-use authorization code gets issued for the client to exchange.
so while a timeout isn't kicking the user off the page, you nailed the real UX gap: there's zero client-server orchestration built for when the human isn't sitting at the keyboard at that exact moment. no push notification, no deep-linking polling to wake up the client, nothing. the boilerplate handles the auth primitives, but not the "notify the user when it's time to approve" UX.
and you're completely right about the end-to-end testing against a real Claude Desktop instance. headless mock suites verify the protocol specs, but they completely miss this kind of real-world human-in-the-loop friction. really appreciate you breaking this down!
MCP-Billing
@abdullah_javaid3 hey abdullah, coming back to this because your case is honestly the one i keep thinking about. you had a real server stuck unauthenticated from the exact gap we talked about, not a hypothetical.
i'm doing a brutally honest review of the launch before deciding what to build next. genuine question, no pitch: if the dynamic loopback fix had already been in place when we talked, would you have actually paid for the boilerplate to unblock that server? or was the OAuth piece specifically something you'd want standalone, separate from the full billing stack? trying to understand if the real gap was that one missing feature, or something about how this is packaged.
the webhook silent-failure story in your maker comment is the one that would actually keep me up at night, not the OAuth stuff everyone's asking about. on the usage-based billing side specifically: if Stripe retries a webhook (which it does on any non-2xx, including your own transient DB hiccups), does mcp-metering dedupe on the event id before incrementing usage, or is double-counting on retry something the integrator has to guard against themselves? that's the kind of bug that doesn't throw an error, it just quietly overcharges someone until they notice their invoice looks wrong
MCP-Billing
@galdayan you're asking the exact right question. double-counting on a retry is the absolute worst kind of bug because everything returns a 200 OK and nobody notices until an invoice looks wrong.
to be totally accurate about where that responsibility lives, there are two distinct surfaces here (one in the standalone mcp-metering package, and one in the full boilerplate):
1. The usage event side (mcp-metering) this is handled via an idempotency key per tool execution, not Stripe's event ID (since the metering package is payment-provider agnostic). same key sent twice = registered exactly once.
the deduplication is guaranteed at the database level, not in app code: an atomic claim() query (INSERT ... ON CONFLICT) happens before executing the callback. it actually uses an ON CONFLICT DO UPDATE scoped to a TTL threshold for pending records, so if a server crashes midway between claiming and settling, the key doesn't turn into a permanent poison pill that blocks future retries.
also, a detail you'll appreciate: the billed: true state is never set before the sink confirms. the strict sequence is finalize(billed: false) → onBillable(event) → markBilled. if your Stripe sync or network call fails midway, the record stays unbilled so you can safely retry it, rather than lying to your DB that it was billed when it wasn't. you can inspect this directly in the npm package code (mcp-metering).
2. The Stripe webhooks side (in the boilerplate) here, yes, there is deduplication by Stripe's event.id via a StripeWebhookEvent table with a hard UNIQUE constraint.
the subtle trick here is that the deduplication marker is written after the handler finishes successfully, not before. if you write it first, a DB crash midway leaves the event marked as processed without actually applying the state change, silent data loss.
we also fixed that webhook bug i mentioned: the handler now strictly splits 400 (invalid signature, permanent failure, don't retry) from 500 (transient DB/network error, Stripe must retry).
the limitation still open (known trade-off): to be 100% transparent with you, there's still a known edge case here. while retries are safe because handlers are idempotent overwrites, Stripe does not guarantee strict delivery order across different events. if an older event gets retried and lands after a newer event was already applied, it could theoretically overwrite the newer state. fixing this with an event.created timestamp check or a manual re-fetch from Stripe is logged in the backlog. in practice the risk window is small at low webhook volume, but it's a real gap and it's documented as such.
really sharp question, this is the exact engineering paranoia that makes billing hard!
@marc_gil1 that event.created ordering gap is the part that would worry me most honestly, more than the retry/dedup stuff which you've clearly thought through carefully. low volume hides it but it doesn't remove it - a webhook queue that gets briefly backed up on your end (a deploy, a cold start) is exactly when out of order delivery gets more likely, not less. do you have a rough sense of how you'd detect it happened after the fact, even before you build the fix? like is there a cheap audit query that would catch a state overwrite from an older event, or would it currently just look like normal data
MCP-Billing
@galdayan honest answer: no, you couldn't detect it today, and it would look like completely normal data.
here is why: the webhook event table only stores event.id as the PK, the type, and processedAt (when my server handled it, not Stripe's event.created). meanwhile, the target resource (like Subscription) only has Prisma's automatic updatedAt. there's no reference to which event wrote the state or what its original timestamp was. so two events processed sequentially in processedAt order could be completely inverted in event.created order, and no query could catch that after the fact.
also, you nailed the real trigger: a deploy or cold start where webhooks queue up and drain in a batch is the scenario where out-of-order delivery actually lands, far more than steady stream traffic.
storing event.created on the target resource fixes both problems at once - it lets you compare timestamps before applying a write and gives you the audit trail to check afterwards.
your question honestly shifted how i view this backlog item: it's not just a rare edge case, it's an uninstrumented one. moving it up.
MCP-Billing
@galdayan hey gal, thanks again for the sharp exchange on Product Hunt last week. your point about the uninstrumented event.created gap completely shifted how i prioritized that backlog item.
i'm reviewing all the launch feedback to understand where the real friction is. as someone who evaluated the technical design deeply: was there anything specific you saw in the stack (including the gap you uncovered) that made it an immediate "no", or do you simply not have the problem this solves right now?