Chuyển đến nội dung chính

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.

Hãy để Sentra viết mã tích hợp cho bạn.
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.
Thử Sentra: Tích hợp Dựa trên AI →
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.
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.
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ẩmGiáTokenQuá hạn
Gói Khởi Đầu29$/tháng10,000,000 token/chu kỳChặn tại không
Gói Chuyên Nghiệp99$/tháng40,000,000 token/chu kỳ0,005$ mỗi 1K token
Gói Nạp Token19$ một lần+5,000,000 token
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

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.
Credits listing page showing created credit entitlements
1

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
2

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 TokensLoại Tín dụng: Chọn Đơn vị Tùy chỉnhTê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)
Độ 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.
3

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

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

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

Open the Meters section

  1. Trong thanh bên của bảng điều khiển, đi đến Sản phẩmMeter
  2. Nhấp vào Tạo Meter
2

Configure the meter

Điền vào:Tên Meter: Token Usage MeterTê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ệnThuộ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
Tên sự kiện là phân biệt chữ hoa thường. api.tokens_usedApi.Tokens.Used — chọn một cái và giữ đúng.
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.
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.

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.
Cấu hình giá Thanh toán Dựa trên Sử dụng

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

1

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 StarterMiê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: MonthlyTiền tệ: USD
2

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
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
Đâ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.
3

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: 10000000Cho phép Quá hạn: Vô hiệu — Khách hàng Gói Khởi Đầu bị chặn khi hết tokenNhậ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
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
Nhấp vào Lưu và sao chép ID sản phẩm.
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.

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

1

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 ProMiêu tả: 40 million API tokens per month with overage. Built for production applications.Giá Cố định: 99.00Chu kỳ Thanh toán: MonthlyTiền tệ: USD
2

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

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: 40000000Nhậ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ẩmCho phép Quá hạn: BậtGiá mỗi đơn vị: 0.000005 USD mỗi token (tức là, 0,005mo^~i1Ktoken,hoc5 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ớiLưu sản phẩm và sao chép ID sản phẩm.
Gói Chuyên Nghiệp: phí cơ bản 99/thaˊng,40Mtoken/chukyˋ,quaˊhnti0,005/tháng, 40M token/chu kỳ, quá hạn tại 0,005/1K token, tự động trừ thông qua meter.

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.
Phần định giá sản phẩm với Thanh toán Đơn chọn
1

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 PackMiêu tả: Instantly add 5 million tokens to your NeuralAPI balance.Giá: 19.00Tiền tệ: USD
2

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

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

Set up your project

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:
tsconfig.json
{
  "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:
package.json
{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}
2

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:
.env
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
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.
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.
3

Implement the server

Tạo src/server.ts:
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');
});
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.
@dodopayments/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, cổng API, lưu trữ đối tượng, luồng, và phạm vi thời gian.
4

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.

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

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

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 hoặc bất kỳ đường hầm nào:
ngrok http 3000
Sao chép URL https://...ngrok-free.app.
2

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
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, vốn ký id.timestamp.body thay vì chỉ thân.

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

1

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

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ả.
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.
3

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.

Khắc phục sự cố

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
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.
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ị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).
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.

Cần hỗ trợ?

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:

Token Credit Entitlement

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

Tiered Plans, One Credit

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

One-Time Top-Up Pack

Khách hàng thêm 5M token cho 19$ mà không thay đổi đăng ký của họ

Auto-Deduction via Meter

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

Live Balance API

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

Verified Webhook Pipeline

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
Đ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/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.

Credit-Based Billing Reference

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.

Credit Webhook Events

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.
Last modified on May 14, 2026