vantezzen/pay

AI agent guide

Instructions for coding agents that integrate vantezzen/pay into an existing app.

This page is written for coding agents. Use it when a user asks you to add vantezzen/pay to an existing project.

If you are a human developer, read the getting started guide instead.

Raw Markdown: /docs/agent.mdx

About

You are being tasked to implement "vantezzen/pay" (or just "pay") into this codebase. pay allows easily charging users for features, consumable credits, or subscriptions. It handles the surrounding work of checkout, webhooks, and credit wallets so that the app can focus on its core functionality. Internally, pay uses Stripe or Polar to handle money, invoices, and taxes.

First, clarify what is being monetized

Before editing files, make sure you know exactly what is being monetized. If the user didn't already provide this, you can ask questions like these, already suggesting your recommendations:

  1. What should users pay for? Name the exact button, route, action, page, API call, or feature.
  2. Should payment buy consumable credits, buy a subscription, or do a one-time payment?
  3. If credits are consumed, how many credits should one paid action cost? What does one "credit" represent in the app (minutes, images, exports, API calls, processed files, etc.)?
  4. Where should wallet or billing controls live: header, sidebar, account menu, settings page, or near the paid action?
  5. Does the app already require login?

Default to credit codes when the app does not already require login. Use external auth only when the app already has stable user accounts and the user wants wallets attached to those accounts.

Read the relevant docs

You can read https://pay.vantezzen.io/docs/llms.txt for list of all docs pages, but the following pages are usually most important:

  • /docs/getting-started/install.mdx covers installation and env vars.
  • /docs/getting-started/first-feature.mdx covers a basic example of gating a feature using credits.
  • /docs/components/provider.mdx covers PayProvider.
  • /docs/components/credit-gate.mdx covers credit-gated actions.
  • /docs/components/feature-gate.mdx covers feature-gated UI.
  • /docs/components/purchase.mdx covers purchase buttons and dialogs.
  • /docs/components/wallet-button.mdx covers anonymous wallet recovery.
  • /docs/concepts/external-auth.mdx covers apps with existing user accounts.

/docs/llms-full.txt will give you the full content of all docs pages. This is very huge and should be used only when the targeted pages are not enough. Append ".mdx" to any docs page path to get the raw Markdown source.

Install the app-side components

For the default credit-code integration, install the common shadcn registry items in the target app:

npx shadcn@latest add \
  https://pay.vantezzen.io/r/pay-provider.json \
  https://pay.vantezzen.io/r/pay-credits.json \
  https://pay.vantezzen.io/r/pay-purchase.json \
  https://pay.vantezzen.io/r/pay-gate.json \
  https://pay.vantezzen.io/r/pay-wallet.json

If the user self-hosts vantezzen/pay, replace https://pay.vantezzen.io with their vantezzen/pay domain.

For external-auth projects, also read /docs/concepts/external-auth.mdx. Those projects usually need the server SDK:

npx shadcn@latest add https://pay.vantezzen.io/r/pay-server.json

Add environment placeholders

Do not invent real keys. Add placeholders to the app's env example and, if the user wants it, to local env files. If you are already supplied with real keys, add those to the real env file, not the example.

.env.local
NEXT_PUBLIC_PAY_URL=https://pay.vantezzen.io
NEXT_PUBLIC_PAY_KEY=pay_pk_replace_me

Here we're using "NEXT_PUBLIC_" to make variables publicly available - if the project doesn't use Next, instead use the framework's public env var prefix.

For self-hosted vantezzen/pay, use the user's own public vantezzen/pay URL.

For external-auth server integrations, use server-only env vars:

.env.local
NEXT_PUBLIC_PAY_URL=https://pay.vantezzen.io
PAY_SECRET_KEY=pay_sk_replace_me

Never expose a pay_sk_... secret key to browser code.

Add PayProvider

Wrap the part of the app that displays balances, opens checkout, or spends credits.

app/layout.tsx
import { PayProvider } from "@/components/pay/provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <PayProvider
          baseUrl={process.env.NEXT_PUBLIC_PAY_URL!}
          publishableKey={process.env.NEXT_PUBLIC_PAY_KEY!}
        >
          {children}
        </PayProvider>
      </body>
    </html>
  );
}

If the app has a narrower authenticated shell or dashboard layout, put PayProvider there instead of wrapping unrelated public marketing pages.

Add wallet and balance UI

Show the current balance in the app shell (e.g. in the header or sidebar) or near paid work:

import { RemainingCredits } from "@/components/pay/remaining-credits";

<RemainingCredits />

For credit-code projects, add wallet recovery somewhere persistent, usually the header, sidebar, account menu, or settings page:

import { WalletButton } from "@/components/pay/wallet-button";

<WalletButton />
// or with custom styling:
<WalletButton variant="outline" size="sm" />

Skip WalletButton only when the project uses external auth and wallets are attached to logged-in users on the server.

Gate paid actions

Next we should gate the paid work itself. Which gate to use depends on whether the paid work is a consumable action or a feature that stays unlocked.

Gate consumable credit actions

Use credits for paid work that is consumed per run, such as minutes, generated images, exports, API calls, or processed files.

components/paid-action.tsx
"use client";

import { CreditGate } from "@/components/pay/credit-gate";
import { PurchaseButton } from "@/components/pay/purchase-dialog";
import { useCredits } from "@/components/pay/provider";
import { Button } from "@/components/ui/button";

const COST = 10;

export function PaidAction() {
  return (
    <CreditGate
      cost={COST}
    >
      <Button disabled={loading}>
        Run paid action
      </Button>
    </CreditGate>
  );
}

Important: CreditGate only checks access. It does not spend credits. Deduct inside the click handler, form action, or job-start path that actually starts the paid work. Use a stable idempotencyKey for retries.

Gate feature access

Use features for access that should stay unlocked while a subscription is active, after a one-time purchase, or after a manual grant.

import { FeatureGate } from "@/components/pay/feature-gate";

export function AnalyticsSection() {
  return (
    <FeatureGate feature="analytics">
      <AnalyticsDashboard />
    </FeatureGate>
  );
}

Use the same feature slug in the vantezzen/pay dashboard when configuring the price that unlocks the feature.

Deduct credits

Next, deduct credits in the paid work path. This is usually a click handler, form action, or job-start path. Deduct only after the user has confirmed the paid action.

When deducing credits, its best to pass a stable idempotencyKey for retryable work, such as a job id or request id. If you omit it, the client generates one, which is fine for a single button click but not for server retries.

Deduct in a client click handler

components/paid-action.tsx
"use client";

import { CreditGate } from "@/components/pay/credit-gate";
import { PurchaseButton } from "@/components/pay/purchase-dialog";
import { useCredits } from "@/components/pay/provider";
import { Button } from "@/components/ui/button";

const COST = 10;

export function PaidAction() {
  const { deduct, loading } = useCredits();

  async function runPaidWork() {
    const jobId = crypto.randomUUID();

    await deduct(COST, {
      idempotencyKey: jobId,
      meta: { feature: "paid-action" },
    });

    // Start the paid work only after deduction succeeds.
  }

  return (
    <CreditGate
      cost={COST}
      fallback={<PurchaseButton>Buy credits</PurchaseButton>}
    >
      <Button disabled={loading} onClick={runPaidWork}>
        Run paid action
      </Button>
    </CreditGate>
  );
}

Deduct in a server action or API route

app/api/run-paid-work/route.ts
import { createPayServerClient } from "@/lib/pay/server";

export const payServer = createPayServerClient({
  baseUrl: process.env.NEXT_PUBLIC_PAY_URL!,
  secretKey: process.env.PAY_SECRET_KEY!, // pay_sk_… - server only
});

export async function GET() {
  const session = await getAuth(); // your auth session
  
  try {
    // 1. Charge for the work (creates the wallet lazily via getOrCreateWallet
    //    on signup, or grant a welcome balance first).
    await payServer.deduct({
      externalUserId: userId,
      amount: 10,
      idempotencyKey: jobId, // retries never double-charge
    });
  } catch (err) {
    if (err instanceof PayError && err.isInsufficientCredits) {
      // 2. Out of credits → send the user to checkout or show a message. The client can also show a purchase dialog instead of redirecting.
      const { url } = await payServer.createCheckout("price_...", {
        externalUserId: userId,
        successUrl: "https://your-app.com/billing?status=success",
      });
      return { needsPayment: url };
    }
    throw err;
  }
  // 3. Run the paid work.
  return startRenderJob(jobId);
}

Or for feature-gated work, check access with hasFeature() instead of deducting credits:

app/api/run-paid-work/route.ts
if (await payServer.hasFeature(user.id, "analytics")) {
  return runAnalyticsExport();
}

Tell the user what to do in vantezzen/pay

After code integration, direct the user to:

https://pay.vantezzen.io/docs/getting-started/dashboard

Tell them to complete these dashboard steps:

  1. If not already, create a vantezzen/pay account and log in.
  2. Follow the Get started steps at the top to connect to Stripe or Polar
  3. Follow the Get started steps at the top to create a new project or open Projects and press New project.
  4. In the new project, follow the First steps guide at the top to create a new product.
  • Here the type should be that same as what we implemented (Credit pack, Subscription or One-time unlock).
  • The name should be the top-level product name (e.g. "Processing time" or "Membership").
  • If its credits, the "Credit unit" should be the matching unit (e.g. "minutes" or "images").
  1. Add a price, pack, or subscription tier matching the code:
    • For CreditGate/Credit features, configure credits granted by the purchased price, e.g. 100 credits for 9.99 USD.
    • For FeatureGate/Subscription or one-time purchases, configure the feature slug used in code (e.g. "analytics").
  2. Open the project Developers tab.
  3. Copy NEXT_PUBLIC_PAY_URL and NEXT_PUBLIC_PAY_KEY into the app env file, replacing the placeholders added earlier.
  • If the app uses external auth, the user also has to click on "Secret key" and copy PAY_SECRET_KEY into the app env file.
  1. Restart the app process if its framework requires env variables to be loaded at startup.

If the user self-hosts vantezzen/pay, tell them to use their own dashboard URL and registry URL instead of pay.vantezzen.io.

Verify the integration

Run the target project's normal checks: typecheck, lint, and focused tests. Do not remove tests to make the integration pass.

Finally, summarize what you did and what the user should do next.