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

# Xây dựng Nền tảng AI API với Thanh toán Dựa trên Tín dụng

> Xây dựng dịch vụ API AI theo tầng với tín dụng token — cấp phát tín dụng qua các gói đăng ký và gói nạp tiền một lần, và tự động trừ khi khách hàng gọi API của bạn.

<Tip>
  <strong>Hãy để Sentra viết mã tích hợp cho bạn.</strong><br />
  Sử dụng trợ lý AI của chúng tôi trong VS Code, Cursor, hoặc Windsurf để tạo mã SDK/API, trình xử lý webhook, cấp phát tín dụng, và nhiều hơn nữa — chỉ cần mô tả những gì bạn muốn.

  <a href="https://dodopayments.com/sentra" target="_blank" rel="noopener noreferrer">
    Thử Sentra: Tích hợp Dựa trên AI →
  </a>
</Tip>

Trong hướng dẫn này, bạn sẽ xây dựng **NeuralAPI** — một nền tảng AI theo tầng, mỗi gói đăng ký đi kèm với hạn mức tín dụng token hàng tháng, khách hàng có thể mua gói nạp tiền khi gần hết, và backend của bạn tự động trừ tín dụng khi các yêu cầu được xử lý bởi OpenAI.

<Note>
  Hướng dẫn này sử dụng Node.js/Express + OpenAI SDK. Các khái niệm của Dodo Payments (tín dụng, meter, webhook) áp dụng cho mọi framework hoặc nhà cung cấp AI — thoải mái thích nghi.
</Note>

Kết thúc hướng dẫn này, bạn sẽ biết cách:

* Tạo chế độ cấp phát tín dụng tùy chỉnh (token) và meter tự động trừ từ đó
* Gắn tín dụng cho các gói đăng ký (có và không có quá hạn) và sản phẩm nạp tiền một lần
* Kết nối điểm kết thúc hoàn thành thực OpenAI mà tính tiền token thông qua Dodo Payments
* Truy vấn số dư tín dụng trực tiếp của khách hàng qua SDK
* Xác minh chữ ký webhook và điều hướng sự kiện tín dụng Dodo Payments

## Điều Chúng Ta Đang Xây Dựng

Đây là mô hình giá cho NeuralAPI:

| Sản phẩm          | Giá          | Token                   | Quá hạn              |
| ----------------- | ------------ | ----------------------- | -------------------- |
| Gói Khởi Đầu      | 29\$/tháng   | 10,000,000 token/chu kỳ | Chặn tại không       |
| Gói Chuyên Nghiệp | 99\$/tháng   | 40,000,000 token/chu kỳ | 0,005\$ mỗi 1K token |
| Gói Nạp Token     | 19\$ một lần | +5,000,000 token        | —                    |

<Info>
  Trước khi bắt đầu, đảm bảo bạn có:

  * Tài khoản Dodo Payments (chế độ kiểm tra là ổn)
  * Khóa API OpenAI
  * Node.js 18+
  * Kiến thức cơ bản về TypeScript/Node.js
</Info>

## Bước 1: Tạo Quyền Tín dụng Token của Bạn

Đầu tiên, tạo quyền tín dụng mà cả hai gói đăng ký và gói nạp sẽ chia sẻ. Hãy xem đây là việc định nghĩa đơn vị "token" mà nền tảng của bạn sử dụng.

<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="Credits listing page showing created credit entitlements" 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. Đăng nhập vào bảng điều khiển Dodo Payments của bạn
    2. Nhấp vào **Sản phẩm** trong thanh bên trái
    3. Chọn tab **Tín dụng**
    4. Nhấp vào **Tạo Tín dụng**
  </Step>

  <Step title="Configure the credit unit">
    Điền chi tiết cơ bản cho tín dụng token của bạn:

    **Tên Tín dụng**: `API Tokens`

    **Loại Tín dụng**: Chọn **Đơn vị Tùy chỉnh**

    **Tên Đơn vị**: `token`

    **Độ chính xác**: `0` (token luôn là số nguyên)

    **Hạn Tín dụng**: `30 days` (tín dụng được làm mới mỗi chu kỳ thanh toán)

    <Warning>
      Độ chính xác không thể thay đổi sau khi đã tạo tín dụng. Đối với số lượng token, `0` (số nguyên) hầu như luôn đúng.
    </Warning>
  </Step>

  <Step title="Skip overage at the credit level">
    Để quá hạn **vô hiệu** ở đây — bạn sẽ cấu hình nó cho từng gói khi gắn tín dụng vào sản phẩm. Điều này cho phép Gói Khởi Đầu chặn khi đạt không trong khi Gói Chuyên Nghiệp cho phép quá hạn.

    <Tip>
      Cài đặt quá hạn được cấu hình ở đây là *mặc định*. Mỗi lần gắn sản phẩm có thể ghi đè chúng — điều mà chúng ta sẽ làm trong Bước 3.
    </Tip>
  </Step>

  <Step title="Save and copy the credit ID">
    Nhấp vào **Tạo Tín dụng**. Sau khi lưu, mở tín dụng và sao chép ID — nó trông giống như `cent_xxxxxxxxxxxx`.

    <Check>
      Quyền tín dụng `API Tokens` của bạn đã sẵn sàng. Tiếp theo, tạo một meter để các sự kiện sử dụng có thể tự động điều khiển các khoản trừ.
    </Check>
  </Step>
</Steps>

## Bước 2: Tạo một Meter cho Việc Sử dụng Token

Một meter tổng hợp các sự kiện sử dụng đến và chuyển đổi chúng thành các khoản trừ tín dụng. Bạn cần điều này *trước* khi tạo các sản phẩm gói, vì bạn sẽ gắn nó trong quá trình tạo sản phẩm ở Bước 3.

<Steps>
  <Step title="Open the Meters section">
    1. Trong thanh bên của bảng điều khiển, đi đến **Sản phẩm** → **Meter**
    2. Nhấp vào **Tạo Meter**
  </Step>

  <Step title="Configure the meter">
    Điền vào:

    **Tên Meter**: `Token Usage Meter`

    **Tên Sự kiện**: `api.tokens_used` *(điều này phải khớp chính xác với những gì ứng dụng của bạn gửi)*

    **Loại Tổng hợp**: `Sum` — chúng tôi tính tổng số lượng token từ mỗi sự kiện

    **Thuộc tính Over**: `tokens` — khóa metadata trên mỗi sự kiện mà giá trị sẽ được tính tổng

    **Đơn vị Đo lường**: `tokens`

    <Warning>
      Tên sự kiện là phân biệt chữ hoa thường. `api.tokens_used` ≠ `Api.Tokens.Used` — chọn một cái và giữ đúng.
    </Warning>

    Lưu meter và sao chép ID của nó — bạn sẽ tham chiếu nó khi gắn vào các sản phẩm.

    <Check>
      Meter đã được tạo. Giờ chúng ta có thể kết nối nó với tín dụng khi cấu hình sản phẩm.
    </Check>
  </Step>
</Steps>

## Bước 3: Tạo Các Sản phẩm Gói

Cả hai gói cần phải là sản phẩm **Thanh toán Dựa trên Sử dụng**, không phải là Đăng ký đơn giản — meter chỉ có thể gắn vào sản phẩm UBB, và bạn cần meter để tự động trừ tín dụng khi khách hàng gọi API của bạn. Sản phẩm UBB vẫn hỗ trợ phí cơ bản định kỳ (`$29` / `$99`); sử dụng trên đó được tính bằng tín dụng.

<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="Cấu hình giá Thanh toán Dựa trên Sử dụng" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB.jpg" />
</Frame>

### Gói Khởi Đầu (29\$/tháng — 10M token, không quá hạn)

<Steps>
  <Step title="Create the Starter UBB product">
    1. Đi đến **Sản phẩm → Tạo Sản phẩm**
    2. Chọn **Thanh toán Dựa trên Sử dụng** làm loại giá
    3. Điền vào:

    **Tên Sản phẩm**: `NeuralAPI Starter`

    **Miêu tả**: `10 million API tokens per month. Perfect for individual developers and small projects.`

    **Giá Cố định**: `29.00` (phí cơ bản định kỳ — tính hàng tháng ngay cả trước khi sử dụng)

    **Chu kỳ Thanh toán**: `Monthly`

    **Tiền tệ**: `USD`
  </Step>

  <Step title="Attach the meter">
    Trong phần **Chọn meter**, nhấp vào **+** và thêm `Token Usage Meter`. Sau đó trên meter:

    1. Chuyển **Tính tiền sử dụng bằng Tín dụng** sang
    2. **Quyền Tín dụng**: chọn `API Tokens`
    3. **Đơn vị meter trên mỗi tín dụng**: `1` — mỗi token trong sự kiện tương ứng với 1 tín dụng bị trừ
    4. **Ngưỡng Miễn phí**: `0` — sự phân bổ tín dụng tự nó là "tầng miễn phí" của khách hàng; chúng tôi không cần thêm một lớp miễn phí khác

    <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="Meter với Tính tiền sử dụng bằng Tín dụng được bật và Token API được chọn" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="2282" data-path="images/CBB/Desktop - Attach Credit - UBB-5.jpg" />
    </Frame>

    Đây là sự kết nối giúp các sự kiện `api.tokens_used` đến thực sự trừ từ số dư của khách hàng.
  </Step>

  <Step title="Configure credit issuance for Starter">
    Vẫn trên sản phẩm, cuộn tới phần cấu hình tín dụng xuất hiện khi một meter được thanh toán bằng tín dụng được gắn:

    **Tín dụng xuất hiện mỗi chu kỳ thanh toán**: `10000000`

    **Cho phép Quá hạn**: **Vô hiệu** — Khách hàng Gói Khởi Đầu bị chặn khi hết token

    **Nhập Cài đặt Tín dụng Mặc định**: Bật — sử dụng hạn 30 ngày từ quyền tín dụng

    <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="Mẫu cấu hình tín dụng với số tiền trên mỗi chu kỳ và cài đặt quá hạn" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB-6.jpg" />
    </Frame>

    Nhấp vào **Lưu** và sao chép ID sản phẩm.

    <Check>
      Gói Khởi Đầu: phí cơ bản 29\$/tháng, 10M token/chu kỳ, chặn tại không, tự động trừ thông qua meter.
    </Check>
  </Step>
</Steps>

### Gói Chuyên Nghiệp (99\$/tháng — 40M token, quá hạn được bật)

<Steps>
  <Step title="Create the Pro UBB product">
    Cùng một quy trình như Gói Khởi Đầu, với số lớn hơn:

    **Tên Sản phẩm**: `NeuralAPI Pro`

    **Miêu tả**: `40 million API tokens per month with overage. Built for production applications.`

    **Giá Cố định**: `99.00`

    **Chu kỳ Thanh toán**: `Monthly`

    **Tiền tệ**: `USD`
  </Step>

  <Step title="Attach the meter">
    Giống như Gói Khởi Đầu: thêm `Token Usage Meter`, chuyển **Tính tiền sử dụng bằng Tín dụng** sang, chọn `API Tokens`, **Đơn vị meter trên mỗi tín dụng** `1`, **Ngưỡng Miễn phí** `0`.
  </Step>

  <Step title="Configure credit issuance with overage">
    Cấu hình cấp phát tín dụng, lần này cho phép quá hạn:

    **Tín dụng xuất hiện mỗi chu kỳ thanh toán**: `40000000`

    **Nhập Cài đặt Tín dụng Mặc định**: **Vô hiệu** — chúng ta cần tùy chỉnh cài đặt quá hạn cho từng sản phẩm

    **Cho phép Quá hạn**: **Bật**

    **Giá mỗi đơn vị**: `0.000005` USD mỗi token (tức là, 0,005$ mỗi 1K token, hoặc 5$ mỗi 1M token — cao hơn tỷ lệ token hiệu quả của gói để hạn chế tràn)

    **Hành vi Quá hạn**: `Bill overage at billing` — quá hạn được tính trên hóa đơn tiếp theo, sau đó số dư được làm mới

    Lưu sản phẩm và sao chép ID sản phẩm.

    <Check>
      Gói Chuyên Nghiệp: phí cơ bản 99$/tháng, 40M token/chu kỳ, quá hạn tại 0,005$/1K token, tự động trừ thông qua meter.
    </Check>
  </Step>
</Steps>

## Bước 4: Tạo Gói Nạp Token

Gói nạp là một giao dịch mua một lần cấp 5,000,000 token vào số dư hiện có của khách hàng.

<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="Phần định giá sản phẩm với Thanh toán Đơn chọn" 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. Đi đến **Sản phẩm → Tạo Sản phẩm**
    2. Chọn **Thanh toán Đơn** là loại giá
    3. Điền vào:

    **Tên Sản phẩm**: `Token Top-Up Pack`

    **Miêu tả**: `Instantly add 5 million tokens to your NeuralAPI balance.`

    **Giá**: `19.00`

    **Tiền tệ**: `USD`
  </Step>

  <Step title="Attach the token credit">
    1. Trong phần **Quyền lợi**, nhấp vào **Gắn** bên cạnh **Tín dụng**
    2. Chọn `API Tokens`
    3. Đặt **Tín dụng xuất hiện**: `5000000`
    4. **Vô hiệu** *Nhập Cài đặt Tín dụng Mặc định* — chúng tôi muốn ghi đè hạn mặc định 30 ngày
    5. Đặt **Hạn Tín dụng**: `365 days`
    6. Lưu sản phẩm

    Sao chép ID sản phẩm.

    <Tip>
      Tại sao lại có hạn dài hơn trên gói nạp? Tín dụng đăng ký được làm mới mỗi 30 ngày do đó là chu kỳ. Gói nạp là *mua trước* — khách hàng đã trả trước 19\$ và kỳ vọng hợp lý rằng các token sẽ kéo dài hơn một tháng. 365 ngày phù hợp với cách tín dụng mua trước thực sự hoạt động tại OpenAI, AWS và Anthropic, trong khi vẫn giới hạn trách nhiệm pháp lý của bạn để khách hàng không thể tích trữ vô thời hạn.
    </Tip>

    <Check>
      Gói Nạp cấu hình xong — việc mua nó cấp 5,000,000 token hợp lệ trong 365 ngày.
    </Check>
  </Step>
</Steps>

## Bước 5: Xây dựng Backend

Bây giờ hãy xây dựng máy chủ Express xử lý thanh toán đăng ký, thanh toán nạp, hoàn thành OpenAI thực với tính tiền token, truy vấn số dư, và sự kiện webhook tín dụng.

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

    Tạo một `tsconfig.json`:

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

    Cập nhật kịch bản `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">
    Tạo `.env` với thông tin đăng nhập của bạn và các ID từ các bước trước:

    ```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>
      Không bao giờ đưa `.env` vào quản lý phiên bản. Thêm nó vào `.gitignore` ngay lập tức.
    </Warning>

    Bạn sẽ điền `DODO_PAYMENTS_WEBHOOK_KEY` ở Bước 7 sau khi đăng ký điểm kết thúc webhook của bạn.
  </Step>

  <Step title="Implement the server">
    Tạo `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 hoàn tất: thanh toán đăng ký, thanh toán nạp, hoàn thành OpenAI với tính tiền token được đo lường, truy vấn số dư và trình xử lý webhook được xác minh.
    </Check>

    <Tip>
      [`@dodopayments/ingestion-blueprints`](/features/usage-based-billing/ingestion-blueprints) cung cấp các tracker tích hợp thả xuống tự động thực hiện cuộc gọi `usageEvents.ingest` cho bạn — bao gồm [LLM Blueprint](/developer-resources/ingestion-blueprints/llm), [cổng API](/developer-resources/ingestion-blueprints/api-gateway), [lưu trữ đối tượng](/developer-resources/ingestion-blueprints/object-storage), [luồng](/developer-resources/ingestion-blueprints/stream), và [phạm vi thời gian](/developer-resources/ingestion-blueprints/time-range).
    </Tip>
  </Step>

  <Step title="A note on how deductions actually happen">
    Bạn có thể đã nhận ra không có cuộc gọi cụ thể "trừ N tín dụng". Điều đó được thiết kế:

    1. Trình xử lý của bạn gọi OpenAI và nhận lại `usage.total_tokens` (ví dụ: 1532).
    2. Bạn lấy một sự kiện sử dụng duy nhất: `event_name: api.tokens_used`, `metadata: { tokens: 1532 }`.
    3. `Token Usage Meter` tổng hợp sự kiện theo khách hàng.
    4. Vì meter được kết nối với tín dụng `API Tokens` với **Tính tiền sử dụng bằng Tín dụng**, Dodo Payments trừ 1532 tín dụng từ khoản cấp phát không hết hạn cũ nhất của khách hàng (FIFO).
    5. Nếu quá hạn được bật và khách hàng giảm dưới không, thiếu hụt được đo lường và tính trên hóa đơn tiếp theo.

    Meter xử lý tất cả điều đó. Mã của bạn chỉ lấy sự kiện.
  </Step>
</Steps>

## Bước 6: Thêm Một Giao Diện Demo

Tạo `public/index.html` để kiểm tra tất cả các luồng trong trình duyệt của bạn. Chúng tôi giữ ID khách hàng vào `localStorage` để đăng ký → tạo → nạp đều chia sẻ cùng một danh tính, giả lập một ứng dụng đã đăng nhập:

<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>

## Bước 7: Kết Nối Webhook

Webhook cho phép máy chủ của bạn phản ứng với thay đổi số dư — bạn sẽ dùng chúng để gửi email "gần hết" trước khi khách hàng đạt không.

<Steps>
  <Step title="Expose your local server">
    Webhook cần một URL công khai. Đối với phát triển cục bộ, sử dụng [ngrok](https://ngrok.com) hoặc bất kỳ đường hầm nào:

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

    Sao chép URL `https://...ngrok-free.app`.
  </Step>

  <Step title="Register the webhook in Dodo Payments">
    1. Trong bảng điều khiển, đi đến **Nhà phát triển → Webhook → Thêm Điểm kết thúc**
    2. **URL**: `https://your-tunnel.ngrok-free.app/webhooks/dodo`
    3. Đăng ký ít nhất với:
       * `credit.added`
       * `credit.deducted`
       * `credit.overage_charged`
    4. Lưu và sao chép **Bí mật Chữ ký**
    5. Dán vào `.env` dưới dạng `DODO_PAYMENTS_WEBHOOK_KEY`, sau đó khởi động lại `npm run dev`

    <Tip>
      `dodo.webhooks.unwrap()` của SDK xác thực `webhook-id`, `webhook-timestamp`, và `webhook-signature` headers sử dụng bí mật chữ ký của bạn. Bạn không cần tự viết mã xác minh HMAC — và bạn không nên, vì Dodo Payments sử dụng [Standard Webhooks](https://www.standardwebhooks.com/), vốn ký `id.timestamp.body` thay vì chỉ thân.
    </Tip>
  </Step>
</Steps>

## Bước 8: Kiểm tra Luồng Đầy Đủ

<Steps>
  <Step title="Subscribe a test customer">
    1. Chạy `npm run dev`
    2. Mở `http://localhost:3000`
    3. Chọn **Gói Pro**, nhập email + tên thử nghiệm, nhấp vào **Nhận Liên kết Thanh toán**, hoàn tất thanh toán bằng [chi tiết thẻ thử nghiệm](/miscellaneous/testing-process)
    4. Trong bảng điều khiển, đi đến **Khách hàng → mới nhất** và sao chép ID `cus_...`
    5. Dán vào trường "ID khách hàng đã đăng nhập" trên bản demo và nhấp vào **Lưu**

    <Check>
      Khách hàng nên có 40,000,000 token. Nhấp vào **Làm mới số dư** để xác nhận.
    </Check>
  </Step>

  <Step title="Generate a real AI response">
    Nhập một lời nhắc và nhấp vào **Tạo**. Máy chủ gọi OpenAI, nhận lại `total_tokens` thực tế, lấy một sự kiện sử dụng, và trả kết quả.

    <Info>
      Sự kiện sử dụng được xử lý bởi một công nhân nền trong mỗi \~phút. Số dư sẽ không giảm ngay lập tức — chờ 30–90 giây và nhấp vào **Làm mới số dư** lần nữa. Đừng kết luận rằng nó bị hỏng nếu lần làm mới đầu tiên không hiển thị sự di chuyển.
    </Info>
  </Step>

  <Step title="Test the top-up flow">
    Nhấp vào **Mua 5M Token — \$19** và hoàn tất thanh toán. Sau khi thanh toán thành công, làm mới số dư — nó sẽ tăng thêm 5,000,000 token. Nhật ký máy chủ của bạn nên hiển thị sự kiện `credit.added`.
  </Step>
</Steps>

## Khắc phục sự cố

<AccordionGroup>
  <Accordion title="Credits not deducting after usage events">
    **Nguyên nhân có thể:**

    * Tên sự kiện của meter không khớp với `event_name` bạn đang gửi (`api.tokens_used` là phân biệt chữ hoa chữ thường)
    * Meter không được liên kết với tín dụng `API Tokens` trên sản phẩm — đi tới cấu hình meter của sản phẩm và xác nhận **Tính tiền sử dụng bằng Tín dụng** được bật
    * Khóa `metadata.tokens` không khớp với trường "Thuộc tính Over" của meter
    * Quyền của khách hàng đã hết hạn (kiểm tra lịch sử tín dụng của khách hàng)

    **Cần kiểm tra:**

    1. **Sản phẩm → Meter**: mở meter và xác nhận nó hiển thị tên tín dụng liên kết trên sản phẩm đã đính kèm
    2. Tab **Sự kiện** trên meter — các sự kiện đã lấy nên xuất hiện ở đó ngay cả trước khi trừ
    3. **Khách hàng → \[Khách hàng] → Tín dụng**: mục nhập sổ sách nên xuất hiện trong vòng một hai phút
  </Accordion>

  <Accordion title="Balance always shows 0 or 'customer not found'">
    **Nguyên nhân có thể:**

    * Khách hàng chưa hoàn tất thanh toán — tín dụng chỉ được cấp phát sau khi thanh toán thành công
    * Bạn đang truy vấn với `customer_id` sai (sử dụng ID `cus_...` từ bảng điều khiển, không phải ID DB của bạn)
    * `CREDIT_ENTITLEMENT_ID` trong `.env` không khớp với tín dụng được đính kèm vào sản phẩm

    **Cần kiểm tra:**
    Mở **Khách hàng → \[Khách hàng] → Tín dụng**. Nếu không có tín dụng xuất hiện ở đó, quyền sản phẩm chưa được đính kèm hoặc thanh toán chưa hoàn tất.
  </Accordion>

  <Accordion title="Overage not working for Pro plan customers">
    **Nguyên nhân có thể:**

    * Quá hạn chưa được bật trên **Cấu hình tín dụng của sản phẩm Pro** (cài đặt cấp tín dụng chỉ là mặc định)
    * Khách hàng thực sự đang dùng Gói Khởi Đầu, không phải Pro
    * Giới hạn quá hạn được đặt thành 0

    **Cần kiểm tra:**
    Chỉnh sửa Pro → Quyền lợi → Tín dụng → xác nhận **Cho phép Quá hạn** được bật và **Giá Mỗi Đơn vị** là `0.000005` (= \$5 mỗi triệu token; kiểm tra lại các số không đứng đầu — trường nhận giá per-token, không phải per-1K).
  </Accordion>

  <Accordion title="`Webhook verification failed` in logs">
    **Nguyên nhân có thể:**

    * Thứ tự phân tích thân: `express.json()` đã được áp dụng cho `/webhooks/dodo` trước `express.raw()` — SDK cần **byte thô** của yêu cầu, không phải JSON đã phân tích
    * Bí mật chữ ký sai trong `DODO_PAYMENTS_WEBHOOK_KEY`
    * Proxy ngược đang viết lại các tiêu đề

    **Cần kiểm tra:**
    Xác nhận dòng `app.use('/webhooks/dodo', express.raw(...))` xuất hiện *trước* `app.use(express.json())` trong `server.ts`.
  </Accordion>
</AccordionGroup>

## Cần hỗ trợ?

* [Cộng đồng Discord](https://discord.gg/bYqAp4ayYh)
* [support@dodopayments.com](mailto:support@dodopayments.com)

## Chúc mừng! Bạn Đã Xây Dựng Hệ thống Thanh toán Dựa trên Tín dụng cho NeuralAPI

Nền tảng của bạn bây giờ đã có hệ thống thanh toán tín dụng hoàn chỉnh và sẵn sàng sản xuất:

<CardGroup cols={2}>
  <Card title="Token Credit Entitlement" icon="coins">
    Tín dụng `API Tokens` dùng lại với hạn 30 ngày, được chia sẻ qua tất cả các gói và gói nạp
  </Card>

  <Card title="Tiered Plans, One Credit" icon="layer-group">
    Gói Khởi Đầu (10M, giới hạn cứng) và Pro (40M + quá hạn) được cấu hình theo từng sản phẩm mà không cần nhân bản tín dụng
  </Card>

  <Card title="One-Time Top-Up Pack" icon="circle-plus">
    Khách hàng thêm 5M token cho 19\$ mà không thay đổi đăng ký của họ
  </Card>

  <Card title="Auto-Deduction via Meter" icon="bolt">
    Số lượng token OpenAI thực được tiếp nhận dưới dạng sự kiện; meter trừ tín dụng FIFO mà không cần theo dõi thủ công
  </Card>

  <Card title="Live Balance API" icon="gauge">
    Số dư theo thời gian thực qua SDK để kiểm soát truy cập, hiển thị sử dụng, hoặc cảnh báo khách hàng trong ứng dụng
  </Card>

  <Card title="Verified Webhook Pipeline" icon="bell">
    Sự kiện sổ cái tín dụng (`credit.added`, `credit.deducted`, `credit.overage_charged`) được điều hướng thông qua trình xử lý xác minh chữ ký sử dụng trợ giúp Standard Webhooks của SDK
  </Card>
</CardGroup>

<Info>
  **Đi vào sản xuất?** Siết chặt những điều này:

  * **Xác thực trên `/credits/:customerId` và `/api/generate`** — hiện tại ai cũng có thể truy cập chúng với bất kỳ ID khách hàng nào. Xác thực người dùng và tra cứu ID khách hàng *của họ* từ phía máy chủ.
  * **Các ID yêu cầu ổn định `event_id`** — ví dụ này sử dụng `Date.now() + random`. Trong sản xuất, sử dụng ID yêu cầu của bạn để đảm bảo các thử lại là idempotent (Dodo Payments loại trừ theo `event_id`).
  * **Lưu trữ ánh xạ khách hàng↔người dùng** — lưu `customer_id` vào DB của bạn sau lần thanh toán đầu tiên để bạn không cần bước dán thủ công.
  * **Quyết định điều gì xảy ra khi một đăng ký kết thúc.** Các tín dụng gói vẫn ở trong sổ cái của khách hàng cho đến khi hết hạn tự nhiên của chúng (30 ngày kể từ khi cấp phát) và tín dụng nạp vẫn có giá trị trong 365 ngày — nhưng `/api/generate` của công thức chỉ kiểm tra *số dư*, không kiểm tra trạng thái đăng ký. Vì vậy, một khách hàng bị hủy bỏ vẫn có thể tiêu thụ số token còn lại. Đó là mặc định thân thiện với người tiêu dùng. Nếu bạn muốn kiểm soát truy cập chặt chẽ hơn, hoặc (a) lắng nghe webhook `subscription.cancelled` và kiểm soát `/api/generate` dựa trên trạng thái đăng ký, hoặc (b) gọi API sổ cái của Dodo để trừ các tín dụng gói chưa sử dụng khi hủy trong khi để lại tín dụng nạp nguyên vẹn.
  * **Giám sát bảng điều khiển Thanh toán Sử dụng** để phát hiện các bất thường trong đo lường sớm.
</Info>

<CardGroup cols={2}>
  <Card title="Credit-Based Billing Reference" icon="book" href="/features/credit-based-billing">
    Tài liệu CBB đầy đủ: chuyển giao, các chế độ quá hạn, quản lý sổ cái, tất cả các điểm cuối API.
  </Card>

  <Card title="Credit Webhook Events" icon="bell" href="/developer-resources/webhooks/intents/credit">
    Sơ đồ tải cho mọi sự kiện tín dụng mà máy chủ của bạn có thể nhận được.
  </Card>
</CardGroup>
