Payment provider router
Skill jacob-balslev/skill-graph/examples/projects/saas-stripe-postgres/skills/payment-provider-router
Skills that know your codebase. Repo-grounded, contract-validated, agent-routable.
npx -y skills add jacob-balslev/skill-graph --skill payment-provider-routerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when dispatching a verified payment event (Stripe webhook or future provider) to the correct downstream handler based on event type. Routes `checkout.session.completed` to subscription provisioning, `invoice.payment_failed` to dunning logic, and `customer.subscription.deleted` to cancellation. Do NOT use for signature verification of the incoming event (use stripe-webhook-signature-verification first) or for the actual subscription database writes (use the per-handler skill or postgres-rls-pattern).
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
10.9 KB, as published. Nobody here has run it
Payment Provider Router
Concept of the skill
What it is: The typed dispatch layer that sends verified payment events to the correct business handler. Mental model: Verification proves the event is authentic; the router decides what business operation the event represents. Why it exists: Payment events are high-impact and retried by providers, so ambiguous routing can duplicate work or miss fulfillment. What it is NOT: It is not webhook signature verification, subscription database writes, or provider SDK setup. Adjacent concepts: Event type maps, handler isolation, provider abstraction, idempotency. One-line analogy: It is the switchboard that sends a verified payment event to the right desk. Common misconception: Unknown events should return an HTTP error; for Stripe, acknowledging and logging unknown-but-valid events prevents retry storms.
Coverage
- The routing table — a typed dispatch map from
Stripe.Event["type"]to handler functions, with a structured "unknown event" fallback that returns 200 (to prevent Stripe retry storms) and logs the unhandled type - Handler isolation — each handler receives only the specific event subtype it needs (e.g.
Stripe.CheckoutSessionCompletedEvent), not the genericStripe.Event, to avoid casts inside handlers - Provider abstraction — how to wrap the Stripe-specific router behind a
PaymentEventcanonical type so a future provider can be added without changing handler code - Error surface — handlers must catch their own errors and return a structured result; an uncaught exception must not produce a 500 that triggers Stripe's retry mechanism with an exponential backoff cascade
- Event type coverage audit — which event types are handled, which are known-ignored (acknowledged with a comment), and which are genuinely unknown
Philosophy of the skill
A payment event router has the same discipline requirement as a content source router: prefer an explicit handler over an implicit fallback, surface unhandled events loudly (in logs, not in HTTP status codes — a 400 triggers a retry, a 200 with a log entry does not), and never let one handler own two semantically distinct events. The event type is the authoritative signal for which business operation to perform; ambiguity at this layer produces double-charges, missed provisioning, and unfired dunning emails.
Routing Rules
// lib/payments/router.ts
import Stripe from "stripe";
import { handleCheckoutComplete } from "./handlers/checkout-complete";
import { handlePaymentFailed } from "./handlers/payment-failed";
import { handleSubscriptionDeleted } from "./handlers/subscription-deleted";
type HandlerResult = { ok: boolean; message?: string };
const EVENT_HANDLERS: Partial<
Record<Stripe.Event["type"], (event: Stripe.Event) => Promise<HandlerResult>>
> = {
"checkout.session.completed": (e) =>
handleCheckoutComplete(e as Stripe.CheckoutSessionCompletedEvent),
"invoice.payment_failed": (e) =>
handlePaymentFailed(e as Stripe.InvoicePaymentFailedEvent),
"customer.subscription.deleted": (e) =>
handleSubscriptionDeleted(e as Stripe.CustomerSubscriptionDeletedEvent),
// Acknowledged non-actionable events — log and return OK
"invoice.paid": async () => ({ ok: true, message: "acknowledged" }),
};
export async function routePaymentEvent(event: Stripe.Event): Promise<HandlerResult> {
const handler = EVENT_HANDLERS[event.type];
if (!handler) {
console.warn("[payment-router] unhandled event type", { type: event.type, id: event.id });
// Return 200 — a 4xx or 5xx would trigger Stripe retry with backoff
return { ok: true, message: "unhandled_event_type" };
}
return handler(event);
}
Routing Decision Rules
| Event type | Handler | Rationale |
|---|---|---|
checkout.session.completed | handleCheckoutComplete | Provision subscription, create org record |
invoice.payment_failed | handlePaymentFailed | Trigger dunning, update subscription status |
customer.subscription.deleted | handleSubscriptionDeleted | Revoke access, archive subscription |
invoice.paid | acknowledged | No action — success is implicit from checkout.session.completed |
| anything else | log + 200 | Unknown event — log for triage, do not retry |
Adding a New Event Type
- Add the Stripe event type string to
EVENT_HANDLERSwith a typed cast. - Write the handler in
lib/payments/handlers/<name>.ts— it receives the specific subtype. - Add a row to the routing table above documenting what the handler does.
- If the event should be intentionally ignored, add it to the "acknowledged" row rather than leaving it in the unknown bucket.
Verification
- Every routable event type is in
EVENT_HANDLERSwith an explicit handler or acknowledgement - Unknown events return 200 (not 400 or 500) to prevent Stripe retry cascades
- Each handler receives a typed subtype, not the generic
Stripe.Event - Handler errors are caught inside the handler and returned as
{ ok: false }— they do not propagate to the router -
routePaymentEventis only called after signature verification (grep forroutePaymentEvent— every call site should be downstream ofconstructEvent)
Do NOT Use When
| Use instead | When |
|---|---|
stripe-webhook-signature-verification | The task is verifying the event's authenticity before routing |
postgres-rls-pattern | The task is writing the database statements inside a specific handler |
| (a generic event bus skill) | The application uses an event bus that is not payment-provider-specific |