> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Crea una plataforma de API de IA con facturación basada en créditos

> Crea un servicio de API de IA escalonado con créditos de tokens: emite créditos mediante planes de suscripción y paquetes de recarga de un solo pago, y descuéntalos automáticamente a medida que los clientes llaman a tu API.

<Tip>
  <strong>Deja que Sentra escriba tu código de integración por ti.</strong><br />
  Usa nuestro asistente de IA en VS Code, Cursor o Windsurf para generar código de SDK/API, controladores de webhooks, asignaciones de créditos y mucho más; solo tienes que describir lo que necesitas.

  <a href="https://dodopayments.com/sentra" target="_blank" rel="noopener noreferrer">
    Prueba Sentra: integración con IA →
  </a>
</Tip>

En este tutorial crearás **NeuralAPI**, una plataforma de IA escalonada en la que cada plan de suscripción incluye una asignación mensual de créditos de tokens, los clientes pueden comprar paquetes de recarga cuando se están quedando sin créditos y tu backend descuenta créditos automáticamente a medida que OpenAI procesa las solicitudes.

<Note>
  Este tutorial utiliza Node.js/Express + el SDK de OpenAI. Los conceptos de Dodo Payments (créditos, medidores y webhooks) se aplican a cualquier framework o proveedor de IA; adáptalos libremente.
</Note>

Al final de este tutorial, sabrás cómo:

* Crear un entitlement de crédito personalizado (tokens) y un medidor que descuente créditos automáticamente
* Asociar créditos a planes de suscripción (con y sin exceso de uso) y a un producto de recarga de un solo pago
* Conectar un endpoint real de completado de OpenAI que facture los tokens mediante Dodo Payments
* Consultar el saldo de créditos actual de un cliente mediante el SDK
* Verificar firmas de webhook y enrutar eventos de crédito de Dodo Payments

## Lo que vamos a crear

Este es el modelo de precios de NeuralAPI:

| Producto                     | Precio           | Tokens                  | Exceso de uso                 |
| ---------------------------- | ---------------- | ----------------------- | ----------------------------- |
| Plan Starter                 | \$29/mes         | 10.000.000 tokens/ciclo | Bloqueado al llegar a cero    |
| Plan Pro                     | \$99/mes         | 40.000.000 tokens/ciclo | \$0.005 por cada 1.000 tokens |
| Paquete de recarga de tokens | \$19, pago único | +5.000.000 tokens       | —                             |

<Info>
  Antes de comenzar, asegúrate de tener:

  * Una cuenta de Dodo Payments (el modo de prueba es suficiente)
  * Una clave de API de OpenAI
  * Node.js 18+
  * Conocimientos básicos de TypeScript/Node.js
</Info>

## Paso 1: Crea tu entitlement de crédito de tokens

Primero, crea el entitlement de crédito que compartirán ambos planes de suscripción y el paquete de recarga. Piensa en esto como la definición de la unidad de «token» que utiliza tu plataforma.

<Frame caption="The Credits tab under Products shows all your credit entitlements.">
  <img src="https://mintcdn.com/dodopayments/eU6ZCQ885P3550bK/images/CBB/Desktop%20-%20Cookbook%20-%20NeuralAPI%20-%20Credit.png?fit=max&auto=format&n=eU6ZCQ885P3550bK&q=85&s=6c34ed3755c78534dcd6012680e98e40" alt="Página de listado de créditos que muestra los entitlements de crédito creados" style={{ maxHeight: '500px', width: 'auto' }} width="2931" height="1665" data-path="images/CBB/Desktop - Cookbook - NeuralAPI - Credit.png" />
</Frame>

<Steps>
  <Step title="Navigate to Credits">
    1. Inicia sesión en tu panel de Dodo Payments
    2. Haz clic en **Products** en la barra lateral izquierda
    3. Selecciona la pestaña **Credits**
    4. Haz clic en **Create Credit**
  </Step>

  <Step title="Configure the credit unit">
    Completa los datos básicos de tu crédito de tokens:

    **Nombre del crédito**: `API Tokens`

    **Tipo de crédito**: selecciona **Custom Unit**

    **Nombre de la unidad**: `token`

    **Precisión**: `0` (los tokens siempre son números enteros)

    **Caducidad del crédito**: `30 days` (los créditos se restablecen en cada ciclo de facturación)

    <Warning>
      La precisión no se puede cambiar después de crear un crédito. Para los recuentos de tokens, `0` (números enteros) casi siempre es la opción correcta.
    </Warning>
  </Step>

  <Step title="Skip overage at the credit level">
    Deja el exceso de uso **deshabilitado** aquí; lo configurarás por plan al asociar el crédito a los productos. Esto permite que el plan Starter bloquee el uso al llegar a cero, mientras que el plan Pro permite el exceso de uso.

    <Tip>
      La configuración del exceso de uso definida aquí es la *predeterminada*. Cada asociación de producto puede anularla; eso es exactamente lo que haremos en el paso 3.
    </Tip>
  </Step>

  <Step title="Save and copy the credit ID">
    Haz clic en **Create Credit**. Una vez guardado, abre el crédito y copia su ID; tiene un aspecto similar a `cent_xxxxxxxxxxxx`.

    <Check>
      Tu entitlement de crédito `API Tokens` está listo. A continuación, crea un medidor para que los eventos de uso activen los descuentos automáticamente.
    </Check>
  </Step>
</Steps>

## Paso 2: Crea un medidor para el uso de tokens

Un medidor agrega los eventos de uso entrantes y los convierte en descuentos de créditos. Lo necesitas *antes* de crear los productos de los planes, ya que lo asociarás durante la creación del producto en el paso 3.

<Steps>
  <Step title="Open the Meters section">
    1. En la barra lateral del panel, ve a **Products** → **Meters**
    2. Haz clic en **Create Meter**
  </Step>

  <Step title="Configure the meter">
    Completa lo siguiente:

    **Nombre del medidor**: `Token Usage Meter`

    **Nombre del evento**: `api.tokens_used` *(debe coincidir exactamente con lo que envía tu aplicación)*

    **Tipo de agregación**: `Sum`; sumamos el recuento de tokens de cada evento

    **Sobre la propiedad**: `tokens`; la clave de metadata de cada evento cuyo valor se sumará

    **Unidad de medición**: `tokens`

    <Warning>
      Los nombres de eventos distinguen entre mayúsculas y minúsculas. `api.tokens_used` ≠ `Api.Tokens.Used`; elige uno y úsalo siempre.
    </Warning>

    Guarda el medidor y copia su ID; lo necesitarás al asociarlo a los productos.

    <Check>
      El medidor se ha creado. Ahora podemos conectarlo al crédito al configurar los productos.
    </Check>
  </Step>
</Steps>

## Paso 3: Crea los productos de los planes

Ambos planes deben ser productos de **Usage Based Billing**, no simples productos de suscripción: los medidores solo se pueden asociar a productos UBB, y necesitas que el medidor descuente créditos automáticamente a medida que los clientes llaman a tu API. Los productos UBB siguen admitiendo una tarifa base recurrente (`$29` / `$99`); el uso adicional se factura en créditos.

<Frame caption="Usage Based Billing pricing type with meter configuration.">
  <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20UBB.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=41b2862c12d126e7843098307e27e137" alt="Configuración de precios de Usage Based Billing" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB.jpg" />
</Frame>

### Plan Starter (\$29/mes — 10 M de tokens, sin exceso de uso)

<Steps>
  <Step title="Create the Starter UBB product">
    1. Ve a **Products → Create Product**
    2. Selecciona **Usage Based Billing** como tipo de precios
    3. Completa lo siguiente:

    **Nombre del producto**: `NeuralAPI Starter`

    **Descripción**: `10 million API tokens per month. Perfect for individual developers and small projects.`

    **Precio fijo**: `29.00` (la tarifa base recurrente, que se factura mensualmente incluso antes de cualquier uso)

    **Ciclo de facturación**: `Monthly`

    **Moneda**: `USD`
  </Step>

  <Step title="Attach the meter">
    En la sección **Select meter**, haz clic en **+** y añade `Token Usage Meter`. Después, en el medidor:

    1. Activa **Bill usage in Credits**
    2. **Entitlement de crédito**: selecciona `API Tokens`
    3. **Unidades del medidor por crédito**: `1`; cada token del evento equivale a 1 crédito descontado
    4. **Umbral gratuito**: `0`; la asignación de créditos es el «nivel gratuito» del cliente, por lo que no necesitamos una banda gratuita adicional

    <Frame caption="Toggle 'Bill usage in Credits' on the meter and pick the credit entitlement.">
      <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20UBB-5.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=b4ef2fe5079cbf3bb39eb3814f101cbd" alt="Medidor con Bill usage in Credits habilitado y API Tokens seleccionados" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="2282" data-path="images/CBB/Desktop - Attach Credit - UBB-5.jpg" />
    </Frame>

    Esta es la conexión que hace que los eventos `api.tokens_used` entrantes descuenten realmente el saldo del cliente.
  </Step>

  <Step title="Configure credit issuance for Starter">
    En el producto, desplázate hasta la sección de configuración de créditos que aparece una vez asociado un medidor facturado por créditos:

    **Créditos emitidos por ciclo de facturación**: `10000000`

    **Permitir exceso de uso**: **Deshabilitado**; los clientes de Starter quedan bloqueados cuando se agotan los tokens

    **Importar configuración de crédito predeterminada**: Habilitado; utiliza la caducidad de 30 días del entitlement de crédito

    <Frame caption="Configure credit issuance per cycle on the UBB product.">
      <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20UBB-6.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=22e99c54f11305a24d63c77e09a4650c" alt="Formulario de configuración de crédito con cantidad por ciclo y configuración de exceso de uso" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB-6.jpg" />
    </Frame>

    Haz clic en **Save** y copia el ID del producto.

    <Check>
      Plan Starter: tarifa base de \$29/mes, 10 M de tokens por ciclo, bloqueado al llegar a cero y con descuentos automáticos mediante el medidor.
    </Check>
  </Step>
</Steps>

### Plan Pro (\$99/mes — 40 M de tokens, exceso de uso habilitado)

<Steps>
  <Step title="Create the Pro UBB product">
    El flujo es el mismo que para Starter, pero con cantidades mayores:

    **Nombre del producto**: `NeuralAPI Pro`

    **Descripción**: `40 million API tokens per month with overage. Built for production applications.`

    **Precio fijo**: `99.00`

    **Ciclo de facturación**: `Monthly`

    **Moneda**: `USD`
  </Step>

  <Step title="Attach the meter">
    Igual que en Starter: añade `Token Usage Meter`, activa **Bill usage in Credits**, selecciona `API Tokens`, establece **Unidades del medidor por crédito** en `1` y **Umbral gratuito** en `0`.
  </Step>

  <Step title="Configure credit issuance with overage">
    Configura la emisión de créditos, esta vez habilitando el exceso de uso:

    **Créditos emitidos por ciclo de facturación**: `40000000`

    **Importar configuración de crédito predeterminada**: **Deshabilitar**; necesitamos personalizar la configuración del exceso de uso por producto

    **Permitir exceso de uso**: **Habilitado**

    **Precio por unidad**: `0.000005` USD por token (es decir, $0.005 por cada 1.000 tokens o $5 por cada millón de tokens; por encima de la tarifa efectiva por token del plan para desincentivar el consumo excedente)

    **Comportamiento del exceso de uso**: `Bill overage at billing`; el exceso de uso se cobra en la siguiente factura y después el saldo se restablece

    Guarda el producto y copia su ID.

    <Check>
      Plan Pro: tarifa base de $99/mes, 40 M de tokens por ciclo, exceso de uso a $0.005 por cada 1.000 tokens y descuentos automáticos mediante el medidor.
    </Check>
  </Step>
</Steps>

## Paso 4: Crea el paquete de recarga de tokens

El paquete de recarga es una compra única que añade 5.000.000 de tokens al saldo de un cliente existente.

<Frame caption="Single Payment pricing selected for a one-time credit product.">
  <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20OTP.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=1743cb3e515952f9d4b1b2782cebac8b" alt="Sección de precios del producto con Single Payment seleccionado" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - OTP.jpg" />
</Frame>

<Steps>
  <Step title="Create a one-time product">
    1. Ve a **Products → Create Product**
    2. Selecciona **Single Payment** como tipo de precios
    3. Completa lo siguiente:

    **Nombre del producto**: `Token Top-Up Pack`

    **Descripción**: `Instantly add 5 million tokens to your NeuralAPI balance.`

    **Precio**: `19.00`

    **Moneda**: `USD`
  </Step>

  <Step title="Attach the token credit">
    1. En la sección **Entitlements**, haz clic en **Attach** junto a **Credits**
    2. Selecciona `API Tokens`
    3. Establece **Créditos emitidos**: `5000000`
    4. **Deshabilita** *Import Default Credit Settings*; queremos anular la caducidad predeterminada de 30 días
    5. Establece **Caducidad del crédito**: `365 days`
    6. Guarda el producto

    Copia el ID del producto.

    <Tip>
      ¿Por qué una caducidad más larga para las recargas? Los créditos de suscripción se restablecen cada 30 días porque ese es el ciclo. Las recargas son *compras prepagadas*: el cliente pagó \$19 por adelantado y espera razonablemente que esos tokens duren más de un mes. Los 365 días se ajustan al funcionamiento de los créditos prepagados reales de OpenAI, AWS y Anthropic, a la vez que limitan tu responsabilidad para evitar que los clientes acumulen créditos indefinidamente.
    </Tip>

    <Check>
      Paquete de recarga configurado: su compra concede 5.000.000 de tokens válidos durante 365 días.
    </Check>
  </Step>
</Steps>

## Paso 5: Crea el backend

Ahora crearemos el servidor Express que gestiona el checkout de suscripciones, el checkout de recargas, los completados reales de OpenAI con facturación por tokens, las consultas de saldo y los eventos de webhook de créditos.

<Steps>
  <Step title="Set up your project">
    ```bash theme={null}
    mkdir neural-api-billing
    cd neural-api-billing
    npm init -y
    npm install dodopayments openai express dotenv
    npm install -D @types/node @types/express typescript tsx
    ```

    Crea un `tsconfig.json`:

    ```json tsconfig.json theme={null}
    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "commonjs",
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true
      }
    }
    ```

    Actualiza los scripts de `package.json`:

    ```json package.json theme={null}
    {
      "scripts": {
        "dev": "tsx watch src/server.ts",
        "build": "tsc",
        "start": "node dist/server.js"
      }
    }
    ```
  </Step>

  <Step title="Set up environment variables">
    Crea `.env` con tus credenciales y los ID de los pasos anteriores:

    ```bash .env theme={null}
    DODO_PAYMENTS_API_KEY=your_dodo_api_key_here
    DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_signing_secret_here
    DODO_ENVIRONMENT=test_mode
    OPENAI_API_KEY=sk-...
    CREDIT_ENTITLEMENT_ID=cent_xxxxxxxxxxxx
    STARTER_PLAN_PRODUCT_ID=pdt_xxxxxxxxxxxx
    PRO_PLAN_PRODUCT_ID=pdt_xxxxxxxxxxxx
    TOPUP_PRODUCT_ID=pdt_xxxxxxxxxxxx
    BASE_URL=http://localhost:3000
    ```

    <Warning>
      Nunca confirmes `.env` en el control de versiones. Añádelo inmediatamente a `.gitignore`.
    </Warning>

    Completarás `DODO_PAYMENTS_WEBHOOK_KEY` en el paso 7, después de registrar tu endpoint de webhook.
  </Step>

  <Step title="Implement the server">
    Crea `src/server.ts`:

    <CodeGroup>
      ```typescript src/server.ts expandable theme={null}
      import 'dotenv/config';
      import DodoPayments from 'dodopayments';
      import OpenAI from 'openai';
      import express, { Request, Response } from 'express';

      const app = express();

      // IMPORTANT: webhook route needs the raw body for signature verification.
      // We register the raw parser ONLY on /webhooks/dodo, then JSON for everything else.
      app.use('/webhooks/dodo', express.raw({ type: 'application/json' }));
      app.use(express.json());
      app.use(express.static('public'));

      const dodo = new DodoPayments({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
        webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
        environment: (process.env.DODO_ENVIRONMENT as 'test_mode' | 'live_mode') ?? 'test_mode',
      });

      const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

      const CREDIT_ENTITLEMENT_ID = process.env.CREDIT_ENTITLEMENT_ID!;
      const BASE_URL = process.env.BASE_URL!;
      const PLAN_PRODUCTS: Record<string, string> = {
        starter: process.env.STARTER_PLAN_PRODUCT_ID!,
        pro: process.env.PRO_PLAN_PRODUCT_ID!,
      };

      // ────────────────────────────────────────────────────────────────────────────
      // Subscription checkout
      // Body: { plan: 'starter' | 'pro', email: string, name: string }
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/checkout/subscribe', async (req: Request, res: Response) => {
        const { plan, email, name } = req.body;
        if (!PLAN_PRODUCTS[plan]) {
          return res.status(400).json({ error: `Unknown plan: ${plan}` });
        }
        try {
          const session = await dodo.checkoutSessions.create({
            product_cart: [{ product_id: PLAN_PRODUCTS[plan], quantity: 1 }],
            customer: { email, name },
            return_url: `${BASE_URL}/?subscribed=1`,
          });
          res.json({ checkout_url: session.checkout_url, session_id: session.session_id });
        } catch (err) {
          console.error('Subscription checkout error:', err);
          res.status(500).json({ error: 'Failed to create subscription checkout' });
        }
      });

      // ────────────────────────────────────────────────────────────────────────────
      // Top-up checkout — buyer must already be a customer
      // Body: { customer_id: string }
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/checkout/topup', async (req: Request, res: Response) => {
        const { customer_id } = req.body;
        if (!customer_id) return res.status(400).json({ error: 'customer_id required' });
        try {
          const session = await dodo.checkoutSessions.create({
            product_cart: [{ product_id: process.env.TOPUP_PRODUCT_ID!, quantity: 1 }],
            customer: { customer_id },
            return_url: `${BASE_URL}/?topup=1`,
          });
          res.json({ checkout_url: session.checkout_url });
        } catch (err) {
          console.error('Top-up checkout error:', err);
          res.status(500).json({ error: 'Failed to create top-up checkout' });
        }
      });

      // ────────────────────────────────────────────────────────────────────────────
      // Live token balance for a customer
      // ────────────────────────────────────────────────────────────────────────────
      app.get('/credits/:customerId', async (req: Request, res: Response) => {
        try {
          const result = await dodo.creditEntitlements.balances.retrieve(req.params.customerId, {
            credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
          });
          res.json({
            balance: result.balance,
            overage: result.overage,
            last_transaction_at: result.last_transaction_at,
          });
        } catch (err) {
          console.error('Balance fetch error:', err);
          res.status(500).json({ error: 'Failed to fetch credit balance' });
        }
      });

      // ────────────────────────────────────────────────────────────────────────────
      // AI completion — calls OpenAI, then ingests a usage event with the real
      // token count. The meter aggregates these and deducts credits automatically.
      // Body: { customer_id: string, prompt: string }
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/api/generate', async (req: Request, res: Response) => {
        const { customer_id, prompt } = req.body;
        if (!customer_id || !prompt) {
          return res.status(400).json({ error: 'customer_id and prompt required' });
        }

        // Best-effort balance gate for Starter (no overage). Note: balance updates
        // are eventually consistent (~1 min lag from event ingestion), so a Starter
        // customer can technically squeeze through a few extra requests right after
        // running out. Use a stricter rate-limiter on top if you need hard cutoffs.
        try {
          const balance = await dodo.creditEntitlements.balances.retrieve(customer_id, {
            credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
          });
          if (Number(balance.balance) <= 0 && Number(balance.overage) <= 0) {
            return res.status(402).json({
              error: 'Out of tokens. Top up or upgrade to continue.',
            });
          }
        } catch {
          // Fall through — if the balance lookup fails, don't block; rely on metering.
        }

        let completion;
        try {
          completion = await openai.chat.completions.create({
            model: 'gpt-5-mini',
            messages: [{ role: 'user', content: prompt }],
          });
        } catch (err) {
          console.error('OpenAI error:', err);
          return res.status(502).json({ error: 'Upstream AI provider failed' });
        }

        const tokensUsed = completion.usage?.total_tokens ?? 0;

        // Fire-and-forget — don't block the response on metering.
        ingestTokenUsage(customer_id, tokensUsed, completion.model).catch((err) =>
          console.error('Usage ingest failed:', err),
        );

        res.json({
          text: completion.choices[0]?.message?.content ?? '',
          tokens_used: tokensUsed,
          model: completion.model,
        });
      });

      async function ingestTokenUsage(customerId: string, tokens: number, model: string) {
        await dodo.usageEvents.ingest({
          events: [
            {
              // event_id is the idempotency key. Use a stable, unique value per request.
              event_id: `req_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
              customer_id: customerId,
              event_name: 'api.tokens_used',
              timestamp: new Date().toISOString(),
              metadata: { tokens, model },
            },
          ],
        });
      }

      // ────────────────────────────────────────────────────────────────────────────
      // Webhook handler — verifies signature using the SDK, then routes events.
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/webhooks/dodo', async (req: Request, res: Response) => {
        const rawBody = (req.body as Buffer).toString('utf8');
        const headers = {
          'webhook-id': req.header('webhook-id') ?? '',
          'webhook-signature': req.header('webhook-signature') ?? '',
          'webhook-timestamp': req.header('webhook-timestamp') ?? '',
        };

        let event: { type: string; data: any };
        try {
          event = dodo.webhooks.unwrap(rawBody, { headers }) as any;
        } catch (err) {
          console.error('Webhook verification failed:', err);
          return res.status(401).json({ error: 'Invalid signature' });
        }

        switch (event.type) {
          case 'credit.added':
            console.log(`[credit.added] customer=${event.data.customer_id} amount=${event.data.amount}`);
            break;
          case 'credit.deducted':
            console.log(`[credit.deducted] customer=${event.data.customer_id} amount=${event.data.amount}`);
            break;
          case 'credit.overage_charged':
            console.log(`[credit.overage_charged] customer=${event.data.customer_id}`);
            break;
          default:
            // Ignore other event types
            break;
        }

        res.json({ received: true });
      });

      app.listen(3000, () => {
        console.log('NeuralAPI billing server running on http://localhost:3000');
      });
      ```

      ```json package.json theme={null}
      {
        "name": "neural-api-billing",
        "version": "1.0.0",
        "scripts": {
          "dev": "tsx watch src/server.ts",
          "build": "tsc",
          "start": "node dist/server.js"
        },
        "dependencies": {
          "dodopayments": "latest",
          "openai": "^4.0.0",
          "express": "^4.18.0",
          "dotenv": "^16.0.0"
        },
        "devDependencies": {
          "@types/node": "^20.0.0",
          "@types/express": "^4.17.0",
          "typescript": "^5.0.0",
          "tsx": "^4.0.0"
        }
      }
      ```
    </CodeGroup>

    <Check>
      Backend terminado: checkout de suscripciones, checkout de recargas, completado de OpenAI con facturación por tokens mediante medidores, consulta de saldo y controlador de webhook verificado.
    </Check>

    <Tip>
      [`@dodopayments/ingestion-blueprints`](/features/usage-based-billing/ingestion-blueprints) proporciona trackers listos para usar que automatizan la llamada `usageEvents.ingest` por ti, incluidos los usos de [LLM Blueprint](/developer-resources/ingestion-blueprints/llm), [API gateway](/developer-resources/ingestion-blueprints/api-gateway), [object storage](/developer-resources/ingestion-blueprints/object-storage), [streams](/developer-resources/ingestion-blueprints/stream) y [time-range](/developer-resources/ingestion-blueprints/time-range).
    </Tip>
  </Step>

  <Step title="A note on how deductions actually happen">
    Quizá hayas notado que no existe una llamada explícita para «descontar N créditos». Está diseñado así:

    1. Tu handler llama a OpenAI y obtiene `usage.total_tokens` (por ejemplo, 1532).
    2. Ingestas un único evento de uso: `event_name: api.tokens_used`, `metadata: { tokens: 1532 }`.
    3. `Token Usage Meter` agrega los eventos por cliente.
    4. Como el medidor está conectado al crédito `API Tokens` con **Bill usage in Credits**, Dodo Payments descuenta 1532 créditos de la asignación no caducada más antigua del cliente (FIFO).
    5. Si el exceso de uso está habilitado y el cliente baja de cero, el déficit se registra y se factura en la siguiente factura.

    El medidor se encarga de todo eso. Tu código solo ingesta eventos.
  </Step>
</Steps>

## Paso 6: Añade un frontend de demostración

Crea `public/index.html` para probar todos los flujos en el navegador. Guardamos el ID del cliente en `localStorage` para que suscribirse → generar → recargar compartan la misma identidad, imitando una aplicación con sesión iniciada:

<CodeGroup>
  ```html public/index.html expandable theme={null}
  <!DOCTYPE html>
  <html>
  <head>
    <title>NeuralAPI Demo</title>
    <style>
      body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 20px; color: #1a1a2e; }
      h1 { font-size: 24px; }
      h2 { margin-top: 36px; border-bottom: 1px solid #eee; padding-bottom: 8px; font-size: 18px; }
      .panel { padding: 16px; background: #fafafe; border: 1px solid #e6e6f0; border-radius: 8px; margin: 12px 0; }
      .form-group { margin: 12px 0; }
      label { display: block; margin-bottom: 4px; font-weight: 600; font-size: 13px; }
      input, select, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-family: inherit; font-size: 14px; }
      textarea { min-height: 80px; resize: vertical; }
      button { background: #6366f1; color: white; padding: 10px 18px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; }
      button:hover { background: #4f46e5; }
      button:disabled { background: #c7c7d4; cursor: not-allowed; }
      .balance { font-size: 32px; font-weight: 700; color: #6366f1; }
      .muted { color: #777; font-size: 13px; margin-top: 4px; }
      .result { margin-top: 12px; padding: 12px; background: #fff; border: 1px solid #e6e6f0; border-radius: 6px; font-size: 14px; white-space: pre-wrap; }
      .row { display: flex; gap: 12px; align-items: center; }
      .row > * { flex: 1; }
    </style>
  </head>
  <body>
    <h1>NeuralAPI Demo</h1>

    <div class="panel">
      <label>Logged-in customer ID (paste once after subscribing)</label>
      <div class="row">
        <input id="customerId" placeholder="cus_xxxxxxxxxxxx" />
        <button onclick="saveCustomerId()" style="flex:0">Save</button>
      </div>
      <div class="muted">After completing checkout, copy the customer ID from your Dodo Payments dashboard (Customers → most recent) and paste here.</div>
    </div>

    <h2>1. Subscribe to a Plan</h2>
    <div class="form-group"><label>Plan</label>
      <select id="plan">
        <option value="starter">Starter — $29/mo, 10M tokens</option>
        <option value="pro">Pro — $99/mo, 40M tokens + overage</option>
      </select>
    </div>
    <div class="form-group"><label>Email</label><input type="email" id="email" placeholder="you@example.com" /></div>
    <div class="form-group"><label>Name</label><input id="name" placeholder="Your name" /></div>
    <button onclick="subscribe(event)">Get Checkout Link</button>
    <div id="subscribeResult" class="result" style="display:none"></div>

    <h2>2. Generate AI Response (deducts tokens)</h2>
    <div class="form-group"><label>Prompt</label><textarea id="prompt" placeholder="Explain quantum computing in one sentence"></textarea></div>
    <button onclick="generate(event)">Generate</button>
    <div id="generateResult" class="result" style="display:none"></div>

    <h2>3. Live Token Balance</h2>
    <button onclick="checkBalance(event)">Refresh Balance</button>
    <div id="balanceResult" class="result" style="display:none"></div>

    <h2>4. Buy a Top-Up Pack</h2>
    <button onclick="topup(event)">Buy 5M Tokens — $19</button>
    <div id="topupResult" class="result" style="display:none"></div>

    <script>
      const $ = (id) => document.getElementById(id);
      document.addEventListener('DOMContentLoaded', () => {
        $('customerId').value = localStorage.getItem('customerId') || '';
      });

      function getCustomerId() {
        const id = $('customerId').value.trim();
        if (!id) { alert('Save a customer ID first'); throw new Error('no customer'); }
        return id;
      }

      function saveCustomerId() {
        localStorage.setItem('customerId', $('customerId').value.trim());
        alert('Saved');
      }

      async function withLoading(btn, loadingLabel, fn) {
        const original = btn.textContent;
        btn.disabled = true;
        btn.textContent = loadingLabel;
        try { await fn(); } finally {
          btn.disabled = false;
          btn.textContent = original;
        }
      }

      async function subscribe(ev) {
        await withLoading(ev.target, 'Loading…', async () => {
          const res = await fetch('/checkout/subscribe', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ plan: $('plan').value, email: $('email').value, name: $('name').value }),
          });
          const data = await res.json();
          const el = $('subscribeResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<a href="${data.checkout_url}" target="_blank">Open Checkout →</a>`
            : `Error: ${data.error}`;
        });
      }

      async function generate(ev) {
        const customer_id = getCustomerId();
        await withLoading(ev.target, 'Generating…', async () => {
          const res = await fetch('/api/generate', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ customer_id, prompt: $('prompt').value }),
          });
          const data = await res.json();
          const el = $('generateResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<strong>Response:</strong>\n${data.text}\n\n<em>Tokens used: ${data.tokens_used} (${data.model})</em>`
            : `Error: ${data.error}`;
          if (res.ok) refreshBalanceSilently();
        });
      }

      async function checkBalance(ev) {
        const customer_id = getCustomerId();
        await withLoading(ev.target, 'Refreshing…', async () => {
          const res = await fetch('/credits/' + customer_id);
          const data = await res.json();
          const el = $('balanceResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<div class="balance">${Number(data.balance).toLocaleString()} tokens</div>
               <div class="muted">Overage used: ${Number(data.overage).toLocaleString()} · Last activity: ${data.last_transaction_at ?? 'never'}</div>`
            : `Error: ${data.error}`;
        });
      }

      async function refreshBalanceSilently() {
        const customer_id = $('customerId').value.trim();
        if (!customer_id) return;
        const res = await fetch('/credits/' + customer_id);
        const data = await res.json();
        const el = $('balanceResult');
        el.style.display = 'block';
        el.innerHTML = res.ok
          ? `<div class="balance">${Number(data.balance).toLocaleString()} tokens</div>
             <div class="muted">Overage used: ${Number(data.overage).toLocaleString()} · Last activity: ${data.last_transaction_at ?? 'never'}</div>`
          : `Error: ${data.error}`;
      }

      async function topup(ev) {
        const customer_id = getCustomerId();
        await withLoading(ev.target, 'Loading…', async () => {
          const res = await fetch('/checkout/topup', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ customer_id }),
          });
          const data = await res.json();
          const el = $('topupResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<a href="${data.checkout_url}" target="_blank">Open Top-Up Checkout →</a>`
            : `Error: ${data.error}`;
        });
      }
    </script>
  </body>
  </html>
  ```
</CodeGroup>

## Paso 7: Conecta el webhook

Los webhooks permiten que tu servidor reaccione a los cambios de saldo; los usarás para enviar correos de «saldo bajo» antes de que los clientes lleguen a cero.

<Steps>
  <Step title="Expose your local server">
    Los webhooks necesitan una URL pública. Para el desarrollo local, utiliza [ngrok](https://ngrok.com) o cualquier túnel:

    ```bash theme={null}
    ngrok http 3000
    ```

    Copia la URL de `https://...ngrok-free.app`.
  </Step>

  <Step title="Register the webhook in Dodo Payments">
    1. En el panel, ve a **Developers → Webhooks → Add Endpoint**
    2. **URL**: `https://your-tunnel.ngrok-free.app/webhooks/dodo`
    3. Suscríbete como mínimo a:
       * `credit.added`
       * `credit.deducted`
       * `credit.overage_charged`
    4. Guarda y copia el **Signing Secret**
    5. Pégalo en `.env` como `DODO_PAYMENTS_WEBHOOK_KEY` y reinicia `npm run dev`

    <Tip>
      El `dodo.webhooks.unwrap()` del SDK valida los encabezados `webhook-id`, `webhook-timestamp` y `webhook-signature` mediante tu secreto de firma. No necesitas implementar manualmente la verificación HMAC; de hecho, no deberías hacerlo, porque Dodo Payments utiliza [Standard Webhooks](https://www.standardwebhooks.com/), que firma `id.timestamp.body` en lugar de firmar únicamente el cuerpo.
    </Tip>
  </Step>
</Steps>

## Paso 8: Prueba el flujo completo

<Steps>
  <Step title="Subscribe a test customer">
    1. Ejecuta `npm run dev`
    2. Abre `http://localhost:3000`
    3. Elige **Plan Pro**, introduce un correo y un nombre de prueba, haz clic en **Get Checkout Link** y completa el checkout con [datos de tarjeta de prueba](/miscellaneous/testing-process)
    4. En el panel, ve a **Customers → most recent** y copia el ID de `cus_...`
    5. Pégalo en el campo «Logged-in customer ID» de la demostración y haz clic en **Save**

    <Check>
      El cliente debería tener 40.000.000 de tokens. Haz clic en **Refresh Balance** para confirmarlo.
    </Check>
  </Step>

  <Step title="Generate a real AI response">
    Escribe un prompt y haz clic en **Generate**. El servidor llama a OpenAI, obtiene el `total_tokens` real, ingesta un evento de uso y devuelve la respuesta.

    <Info>
      Los eventos de uso se procesan mediante un worker en segundo plano aproximadamente cada minuto. El saldo no disminuirá al instante: espera entre 30 y 90 segundos y vuelve a hacer clic en **Refresh Balance**. No concluyas que algo está roto si la primera actualización no muestra cambios.
    </Info>
  </Step>

  <Step title="Test the top-up flow">
    Haz clic en **Buy 5M Tokens — \$19** y completa el checkout. Cuando el pago se haya realizado correctamente, actualiza el saldo; debería aumentar en 5.000.000 de tokens. El registro del servidor debería mostrar un evento `credit.added`.
  </Step>
</Steps>

## Solución de problemas

<AccordionGroup>
  <Accordion title="Credits not deducting after usage events">
    **Posibles causas:**

    * El nombre del evento del medidor no coincide con el `event_name` que estás enviando (`api.tokens_used` distingue entre mayúsculas y minúsculas)
    * El medidor no está vinculado al crédito `API Tokens` del producto; ve a la configuración del medidor del producto y confirma que **Bill usage in Credits** esté habilitado
    * La clave `metadata.tokens` no coincide con el campo «Over Property» del medidor
    * La asignación del cliente ha caducado (consulta el historial de créditos del cliente)

    **Qué comprobar:**

    1. **Products → Meters**: abre el medidor y confirma que muestre el nombre del crédito vinculado en la asociación del producto
    2. La pestaña **Events** del medidor; los eventos ingeridos deberían aparecer allí incluso antes del descuento
    3. **Customers → \[Customer] → Credits**: las entradas del ledger deberían aparecer en uno o dos minutos
  </Accordion>

  <Accordion title="Balance always shows 0 or 'customer not found'">
    **Posibles causas:**

    * El cliente aún no ha completado el checkout; los créditos solo se emiten después de un pago correcto
    * Estás consultando con el `customer_id` incorrecto (utiliza el ID `cus_...` del panel, no el ID de tu propia base de datos)
    * El `CREDIT_ENTITLEMENT_ID` en `.env` no coincide con el crédito asociado al producto

    **Qué comprobar:**
    Abre **Customers → \[Customer] → Credits**. Si no aparecen créditos, el entitlement del producto no se asoció o el pago no se completó.
  </Accordion>

  <Accordion title="Overage not working for Pro plan customers">
    **Posibles causas:**

    * El exceso de uso no se habilitó en la **asociación de crédito del producto Pro** (la configuración del nivel de crédito solo es un valor predeterminado)
    * El cliente está realmente en Starter, no en Pro
    * El límite de exceso de uso se estableció en 0

    **Qué comprobar:**
    Edita Pro → Entitlements → Credits → confirma que **Allow Overage** esté habilitado y que **Price Per Unit** sea `0.000005` (= \$5 por cada millón de tokens; comprueba los ceros iniciales: el campo acepta el precio por token, no por cada 1.000 tokens).
  </Accordion>

  <Accordion title="`Webhook verification failed` in logs">
    **Posibles causas:**

    * Orden del análisis del cuerpo: `express.json()` se aplicó a `/webhooks/dodo` antes que `express.raw()`; el SDK necesita los **bytes sin procesar** de la solicitud, no JSON analizado
    * Secreto de firma incorrecto en `DODO_PAYMENTS_WEBHOOK_KEY`
    * El proxy inverso está reescribiendo los encabezados

    **Qué comprobar:**
    Confirma que la línea `app.use('/webhooks/dodo', express.raw(...))` aparezca *antes* que `app.use(express.json())` en `server.ts`.
  </Accordion>
</AccordionGroup>

## ¿Necesitas ayuda?

* [Comunidad de Discord](https://discord.gg/bYqAp4ayYh)
* [support@dodopayments.com](mailto:support@dodopayments.com)

## ¡Enhorabuena! Has creado la facturación basada en créditos para NeuralAPI

Tu plataforma ahora cuenta con un sistema completo de facturación de créditos listo para producción:

<CardGroup cols={2}>
  <Card title="Token Credit Entitlement" icon="coins">
    Un crédito `API Tokens` reutilizable con una caducidad de 30 días, compartido entre todos los planes y el paquete de recarga
  </Card>

  <Card title="Tiered Plans, One Credit" icon="layer-group">
    Starter (10 M, límite estricto) y Pro (40 M + exceso de uso), configurados por producto sin duplicar el crédito
  </Card>

  <Card title="One-Time Top-Up Pack" icon="circle-plus">
    Los clientes añaden 5 M de tokens por \$19 sin cambiar su suscripción
  </Card>

  <Card title="Auto-Deduction via Meter" icon="bolt">
    Los recuentos reales de tokens de OpenAI se ingieren como eventos; el medidor descuenta créditos mediante FIFO sin seguimiento manual
  </Card>

  <Card title="Live Balance API" icon="gauge">
    Saldo en tiempo real mediante el SDK para controlar el acceso, mostrar el uso o advertir a los clientes dentro de la aplicación
  </Card>

  <Card title="Verified Webhook Pipeline" icon="bell">
    Eventos del ledger de créditos (`credit.added`, `credit.deducted`, `credit.overage_charged`) enrutados mediante un handler cuya firma se verifica utilizando el helper Standard Webhooks del SDK
  </Card>
</CardGroup>

<Info>
  **¿Vas a pasar a producción?** Refuerza estos aspectos:

  * **Autenticación en `/credits/:customerId` y `/api/generate`**; actualmente cualquiera puede acceder a ellos con cualquier ID de cliente. Autentica a los usuarios y busca su ID de cliente en el servidor.
  * **`event_id`s estables**; el ejemplo utiliza `Date.now() + random`. En producción, usa el ID de tu solicitud para que los reintentos sean idempotentes (Dodo Payments deduplica por `event_id`).
  * **Persistencia de la relación cliente↔usuario**; guarda `customer_id` en tu base de datos después del primer checkout para no tener que pegarlo manualmente.
  * **Decide qué ocurre cuando termina una suscripción.** Los créditos del plan permanecen en el ledger del cliente hasta su caducidad natural (30 días desde su emisión) y los créditos de recarga son válidos durante 365 días; sin embargo, `/api/generate` del cookbook solo comprueba el *saldo*, no el estado de la suscripción. Por tanto, un cliente cancelado aún puede consumir los tokens restantes. Ese es el comportamiento predeterminado más favorable para el consumidor. Si quieres un control de acceso más estricto, (a) escucha el webhook `subscription.cancelled` y controla `/api/generate` según el estado de la suscripción, o (b) llama a la API de ledger de Dodo para debitar los créditos no utilizados del plan al cancelar, dejando intactos los créditos de recarga.
  * **Supervisa el panel de Usage Billing** para detectar pronto anomalías en la medición.
</Info>

<CardGroup cols={2}>
  <Card title="Credit-Based Billing Reference" icon="book" href="/features/credit-based-billing">
    Documentación completa de CBB: rollover, modos de exceso de uso, gestión del ledger y todos los endpoints de API.
  </Card>

  <Card title="Credit Webhook Events" icon="bell" href="/developer-resources/webhooks/intents/credit">
    Esquemas de payload para cada evento de crédito que pueda recibir tu servidor.
  </Card>
</CardGroup>
