# AI agent guide (/docs/agent)



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](/docs/getting-started/index.mdx) instead.

Raw Markdown: [`/docs/agent.mdx`](/docs/agent.mdx)

## About [#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 [#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 [#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 [#install-the-app-side-components]

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

```bash
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:

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

## Add environment placeholders [#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.

```bash title=".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:

```bash title=".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 [#add-payprovider]

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

```tsx title="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 [#add-wallet-and-balance-ui]

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

```tsx
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:

```tsx
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 [#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 [#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.

```tsx title="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 [#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.

```tsx
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 [#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 [#deduct-in-a-client-click-handler]

```tsx title="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 [#deduct-in-a-server-action-or-api-route]

```ts title="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:

```ts title="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 [#tell-the-user-what-to-do-in-vantezzenpay]

After code integration, direct the user to:

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

Tell them to complete these dashboard steps:

0. If not already, create a vantezzen/pay account and log in.
1. Follow the **Get started** steps at the top to connect to Stripe or Polar
2. Follow the **Get started** steps at the top to create a new project or open **Projects** and press **New project**.
3. 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").

6. 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").
7. Open the project **Developers** tab.
8. 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.

9. 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 [#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.
