@uriva/p2b-stripe
1.0.2Description
Stripe subscription and payment billing integration for prompt2bot agents — tool design, Stripe checkout links, dynamic customer portal sessions, webhooks, and secure sandbox routing..
tank install -g @uriva/p2b-stripeVerified clean
No security issues detected in the latest scan.
p2b-stripe
Stripe subscription and payment billing integration skill for prompt2bot agents. Covers tool design, checkout generation, dynamic portal session handling, webhook validation, and database updates.
Instructions
1. Structure of Agent Billing Tools
To enable agents to process payments natively during conversation, the bot must
expose a dedicated billing skill with two primary tools:
A. Checkout Link Generation (generate_stripe_checkout_link)
- Purpose: Generates a Stripe Checkout URL for users to purchase or upgrade to a premium/Pro tier.
- Dynamic Tier & Price ID Selection:
- The tool should accept an optional
tierorpriceIdparameter. This allows the bot to dynamically generate checkout sessions matching the specific tier desired by the user (e.g."50_credits","150_credits", or"500_credits").
- The tool should accept an optional
- Stripe API Key Permissions (
sk_vsrk_):- To dynamically generate checkout sessions, Stripe's API requires an API key
with "Checkout Sessions Write" (
checkout_session_write) and "Prices Read" (plan_read) permissions. - If the configured key is a Restricted API Key (
rk_live_...) lacking these permissions, Stripe will return aPermission Deniederror.
- To dynamically generate checkout sessions, Stripe's API requires an API key
with "Checkout Sessions Write" (
- Implementation & Fallback:
- Call the Stripe SDK
checkout.sessions.createAPI with the selectedpriceIdand the user'scustomer_email. - If the secret override
STRIPE_PRO_LINKis set, return it immediately. - Graceful Fallback: If the API key lacks permissions or is missing, catch
the error and fall back gracefully to a static Stripe Payment Link
(
https://buy.stripe.com/...URL) so the user is never blocked.
- Call the Stripe SDK
// Dynamic Checkout Session Generation with Fallback
export const generateCheckoutSessionLink = async (
priceId: string,
email: string | null | undefined,
): Promise<string> => {
const apiKey = await getSecret("STRIPE_SECRET_KEY")
.catch(() => getSecret("STRIPE_API_KEY"))
.catch(() => "");
if (!apiKey) {
throw new Error("STRIPE_SECRET_KEY is missing.");
}
const stripeClient = new Stripe(apiKey);
const session = await stripeClient.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card", "link"],
line_items: [{ price: priceId, quantity: 1 }],
...(email && { customer_email: email }),
success_url: "https://your-app.com/settings?checkout=success",
cancel_url: "https://your-app.com/pricing?checkout=cancelled",
});
return session.url!;
};
// Example Tool Handler
defineTool({
name: "generate_stripe_checkout_link",
description:
"Generate a Stripe checkout link. Optional 'tier' parameter allows selecting custom tiers.",
parameters: z.object({
tier: z.enum(["tier_1", "tier_2", "tier_3"]).optional(),
}),
handler: async ({ tier }) => {
const defaultPriceId = "price_YOUR_PRICE_ID_1";
const priceIdMap = {
"tier_1": "price_YOUR_PRICE_ID_1",
"tier_2": "price_YOUR_PRICE_ID_2",
"tier_3": "price_YOUR_PRICE_ID_3",
};
const priceId = priceIdMap[tier || "tier_1"] || defaultPriceId;
const defaultFallback = "https://buy.stripe.com/YOUR_DEFAULT_PAYMENT_LINK";
try {
const link = await getSecret("STRIPE_PRO_LINK").catch(() => "") ||
await generateCheckoutSessionLink(
priceId,
await resolveEmail(userId()),
);
return { url: link };
} catch (error) {
console.error(
"Failed to generate dynamic Stripe checkout session:",
error,
);
return { url: defaultFallback };
}
},
});
B. Customer Portal Link Generation (generate_stripe_portal_link)
- Purpose: Generates a Stripe Customer Portal link so users can securely cancel, update, or manage their subscriptions.
- CRITICAL IMPLEMENTATION DETAIL:
- Never return a static, hardcoded hosted login portal URL like
https://billing.stripe.com/p/login/acct_1OVx4SHiG8UAKPQv. These links return HTTP 404 unless the customer portal login page has been explicitly activated in the Stripe Dashboard. - Dynamic Portal Session Generation: Instead, dynamically create a pre-authenticated, short-lived portal session using the Stripe SDK.
- The Lookup Flow:
- Resolve the current user's email address from their conversation state.
- Check the database for a stored
stripeCustomerId. If not found, call Stripe's Customer List API (stripe.customers.list({ email, limit: 1 })) to find the matching Customer ID by email dynamically. - Call the Stripe Billing Portal API
(
stripe.billingPortal.sessions.create({ customer: customerId, return_url })) to generate the portal session URL. - Fall back to the configured default hosted login portal ONLY if
STRIPE_SECRET_KEYis missing or the dynamic creation fails.
- Never return a static, hardcoded hosted login portal URL like
// Dynamic Portal Session Generation Code
import Stripe from "stripe";
export const generateCustomerPortalLink = async (
email: string,
): Promise<string> => {
const defaultFallback =
"https://billing.stripe.com/p/login/acct_1OVx4SHiG8UAKPQv";
try {
const apiKey = await getSecret("STRIPE_SECRET_KEY")
.catch(() => getSecret("STRIPE_API_KEY"))
.catch(() => "");
if (!apiKey) return defaultFallback;
const stripeClient = new Stripe(apiKey);
const customers = await stripeClient.customers.list({ email, limit: 1 });
if (!customers.data || customers.data.length === 0) return defaultFallback;
const customerId = customers.data[0].id;
const session = await stripeClient.billingPortal.sessions.create({
customer: customerId,
return_url: "https://your-app.com",
});
return session.url;
} catch (error) {
console.error("Failed to generate dynamic customer portal link:", error);
return defaultFallback;
}
};
2. Webhook Subscription & Verification
To securely handle payment events from Stripe (such as initial checkouts and
monthly recurring renewals), you must configure a secure /stripe webhook
endpoint that receives POST requests directly from Stripe.
Webhook Signature Verification
To prevent spoofing, every webhook payload must be cryptographically verified
using the stripe-signature header and your configured STRIPE_WEBHOOK_SECRET.
- The
stripe-signatureheader contains a timestamp (t=...) and one or more signatures (v1=...). - You must compute a SHA256 HMAC of the payload concatened with the timestamp, using the webhook secret, and compare it with the signature.
import { createHmac } from "node:crypto";
export const verifyStripeWebhook = (
rawBody: string,
signatureHeader: string,
webhookSecret: string,
): boolean => {
try {
const parts = signatureHeader.split(",");
const tPart = parts.find((p) => p.startsWith("t="));
const v1Part = parts.find((p) => p.startsWith("v1="));
if (!tPart || !v1Part) return false;
const timestamp = tPart.substring(2);
const signature = v1Part.substring(3);
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = createHmac("sha256", webhookSecret)
.update(signedPayload)
.digest("hex");
return expectedSignature === signature;
} catch {
return false;
}
};
3. Handling Webhook Events
You must listen for the following two core webhook events to handle user subscriptions and recurring renewals:
A. checkout.session.completed
Fired when a user successfully completes a Stripe Checkout session. This event is responsible for provisioning the initial Pro tier access and sending a welcome email.
B. invoice.payment_succeeded
Fired every time a recurring monthly subscription invoice is successfully paid. This event is responsible for maintaining the user's Pro status or adding search credits.
Processing Pipeline:
- Read the event
typeand thedata.object. - Extract the customer's email from
customer_details.emailorcustomer_email. - Verify that the purchased items contain one of your allowed
price_IDs. If the checkout contains line items, usestripe.checkout.sessions.lineItems.list(sessionId)to fetch and verify the price IDs. - Record the payment event in the database (e.g. adding a new entry to the
paymentEventstable withamountUSD,type: "stripe", andtimestamp). - Provision/upgrade the user's tier or credits, and send a welcome email.
// Stripe Webhook Event Processor Example
export const stripeHandler = async (payload: StripeEvent) => {
const eventType = payload.type;
const dataObject = payload.data?.object;
if (!dataObject) return;
let email = dataObject.customer_details?.email || dataObject.customer_email;
let amountCents = dataObject.amount_total || dataObject.amount_paid || 0;
if (!email) return;
const allowedPriceIds = [
"price_YOUR_PRICE_ID_1",
"price_YOUR_PRICE_ID_2",
"price_YOUR_PRICE_ID_3",
];
let hasMatchingPrice = false;
if (eventType === "invoice.payment_succeeded") {
const invoiceLines = dataObject.lines?.data || [];
hasMatchingPrice = invoiceLines.some((line) =>
allowedPriceIds.includes(line.price?.id)
);
} else if (eventType === "checkout.session.completed") {
const stripeApiKey = await getSecret("STRIPE_SECRET_KEY").catch(() => "");
if (stripeApiKey) {
const lineItems = await fetchCheckoutSessionLineItems(
dataObject.id,
stripeApiKey,
);
if (lineItems) {
hasMatchingPrice = lineItems.some((line) =>
allowedPriceIds.includes(line.price?.id)
);
}
}
}
if (!hasMatchingPrice) return;
// Add payment event to database
await addPaymentEvent(email, "stripe", amountCents / 100);
if (eventType === "checkout.session.completed") {
await sendWelcomeToProEmail(email);
}
};
4. Security & Sandbox Routing Best Practices
When running Stripe billing integrations inside AI-agent-built apps, strictly adhere to these security mandates:
- Universal Secrets Configuration: Always configure Stripe secrets in
Infisical or as secure environment variables. Never commit raw Stripe keys
(
sk_live_...orsk_test_...) to the codebase. - Allowed Hosts Filtering: Ensure that the Stripe webhook verification
endpoint and client API calls only accept connections originating from the
official Stripe IP ranges and domain names (
api.stripe.com,checkout.stripe.com,billing.stripe.com). - Allowed Hosts Proxy: In sandboxed programming environments (like
Safescript), tag all Stripe secrets as Stripe-only (restricting outbound
network egress to the
api.stripe.comhost) to eliminate supply chain attack vectors and prevent token exfiltration.