Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for managing customer records, saved payment methods, monetary wallets, and hosted customer-portal sessions; subscription lifecycle and custom billing UI are covered separately.
.claude/skills/hashgraph-online-customer-management/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 41% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 109% | 0% |
Build customer records, hosted self-service portals, saved payment methods, and real-money wallet ledgers. This skill covers the full customer lifecycle: CRUD operations, payment method management, time-bound portal sessions, and idempotent wallet transactions.
Customers are records in Dodo that group payments, subscriptions, and portal access. Each customer has an ID (prefix cus_), email, name, and optional metadata.
Customer Portal is a hosted, time-bound session that lets customers self-serve: view invoices, cancel subscriptions, change plans, update payment methods, recover on-hold subscriptions, and download license keys. You create a session and redirect to its link.
Payment Methods are saved cards or other payment instruments linked to a customer. You can list and delete them; creation happens during checkout.
Customer Wallets hold real-money balances (USD, INR) that customers can spend on future purchases. They are NOT the same as usage credits (see credit-based-billing skill). Wallet ledger entries are idempotent via idempotency_key to prevent duplicate charges.
Entitlements are feature or file grants. listCreditEntitlements returns credit balances; listEntitlements returns feature/file grants; listEntitlementGrants lists individual grants with revocation status.
Create, list, retrieve, and update customer records.
typescriptimport DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', }); // Create a customer const customer = await client.customers.create({ email: 'alice@example.com', name: 'Alice Chen', metadata: { userId: 'user_12345', tier: 'premium', }, }); console.log(customer.customer_id); // cus_... // List customers const customers = await client.customers.list({ page_size: 10, }); // Retrieve a customer const retrieved = await client.customers.retrieve('cus_abc123'); // Update a customer await client.customers.update('cus_abc123', { name: 'Alice Chen-Smith', metadata: { tier: 'enterprise' }, });
Create a time-bound session that redirects customers to a hosted portal. The portal is read-only for most actions; customers can view invoices, cancel subscriptions, change plans, update payment methods, recover on-hold subscriptions, and download license keys.
typescript// Create a portal session const session = await client.customers.customerPortal.create('cus_abc123', { return_url: 'https://yourapp.com/dashboard', }); // Redirect the customer window.location.href = session.link;
The portal session link expires after a short time. Customers cannot:
If you need to rebuild any of these, you're duplicating the portal. Use it instead.
List and delete payment methods saved to a customer.
typescript// List payment methods for a customer const methods = await client.customers.retrievePaymentMethods('cus_abc123'); console.log(methods); // { // items: [{ payment_method_id: 'pm_xyz789', payment_method: 'card' }] // } // Delete a payment method await client.customers.deletePaymentMethod('pm_xyz789', { customer_id: 'cus_abc123', });
Payment methods are created during checkout when the customer opts to save their card. You cannot create them directly via the API.
Wallets hold real-money balances (USD, INR) that customers can spend on future purchases. They are distinct from usage credits. List wallet balances and create idempotent ledger entries.
typescript// List wallets for a customer const wallets = await client.customers.wallets.list('cus_abc123'); console.log(wallets); // { // items: [ // { currency: 'USD', balance: 5000, customer_id: 'cus_abc123', ... }, // { currency: 'INR', balance: 100000, customer_id: 'cus_abc123', ... } // ], // total_balance_usd: 17000 // } // List ledger entries const entries = await client.customers.wallets.ledgerEntries.list('cus_abc123', { page_size: 20, }); // Create a ledger entry (credit or debit) // Amounts are in the smallest currency unit (cents for USD, paise for INR) await client.customers.wallets.ledgerEntries.create('cus_abc123', { amount: 1000, // $10.00 in USD currency: 'USD', entry_type: 'credit', // or 'debit' reason: 'Refund for order #12345', idempotency_key: 'refund_order_12345_v1', });
Idempotency is critical. If your network fails after creating a ledger entry, retrying with the same idempotency_key returns the existing entry instead of creating a duplicate charge. Always use a stable, unique key per transaction.
Wallets are real money. Do not create entries without idempotency keys, and do not expose wallet balances to the client without verification.
Entitlements are feature or file grants. There are three methods:
typescript// List credit entitlements (credit balances) const credits = await client.customers.listCreditEntitlements('cus_abc123'); console.log(credits); // { // items: [{ // credit_entitlement_id: 'cred_ent_1', // balance: '5000', // name: 'API Calls', // overage: '0', // unit: 'calls' // }] // } // List feature/file entitlements const features = await client.customers.listEntitlements('cus_abc123'); console.log(features); // { // items: [ // { // grant_id: 'entg_feature_1', // entitlement_id: 'ent_feature_1', // entitlement_name: 'Pro Features', // integration_type: 'feature_flag', // status: 'delivered', // created_at: '2026-01-01T00:00:00Z', // updated_at: '2026-01-01T00:00:00Z' // } // ] // } // List individual grants (with revocation status) const grants = await client.customers.listEntitlementGrants('cus_abc123', { page_size: 50, }); console.log(grants); // { // items: [{ // id: 'entg_feature_1', // entitlement_id: 'ent_feature_1', // status: 'Delivered', // revoked_at: null // }] // }
Store the Dodo customer ID in your user record, or use metadata to link them.
typescript// Option 1: Store the Dodo customer ID in your database const customer = await client.customers.create({ email: user.email, name: user.name, }); await db.users.update(user.id, { dodo_customer_id: customer.customer_id }); // Option 2: Use metadata to store your user ID const customer = await client.customers.create({ email: user.email, name: user.name, metadata: { app_user_id: user.id, }, }); // Later, retrieve by email (not a direct API call, but common pattern) // You must store the mapping yourself or query Dodo's list endpoint const customers = await client.customers.list({ page_size: 100 }); const found = customers.items.find(c => c.email === user.email);
Email matching pitfall: if a user changes their email in your app, the Dodo customer email won't update automatically. Always sync email changes explicitly via client.customers.update().
Creating duplicate customers per checkout. Each checkout should reuse an existing customer_id or create the customer once. Creating a new customer for every transaction fragments your customer data.
typescript// WRONG const session = await client.checkoutSessions.create({ product_cart: [...], customer: { email: user.email }, // Creates a new customer each time }); // RIGHT const customer = await client.customers.create({ email: user.email, name: user.name, }); const session = await client.checkoutSessions.create({ product_cart: [...], customer: { customer_id: customer.customer_id }, // Reuse the customer });
Building a bespoke billing UI instead of using the portal. The portal handles invoices, cancellations, plan changes, payment method updates, and on-hold recovery. If you rebuild these, you're duplicating work and missing edge cases.
Mutating wallet balances without an idempotency key. Retries will create duplicate entries and charge the customer twice.
typescript// WRONG await client.customers.wallets.ledgerEntries.create('cus_abc123', { amount: 1000, currency: 'USD', entry_type: 'credit', reason: 'Refund', // No idempotency_key }); // RIGHT await client.customers.wallets.ledgerEntries.create('cus_abc123', { amount: 1000, currency: 'USD', entry_type: 'credit', reason: 'Refund', idempotency_key: 'refund_order_12345_v1', });
Exposing portal links publicly. Portal sessions are time-bound and tied to a customer ID. Don't log them or share them in URLs; generate them server-side and redirect immediately.
Confusing wallets with credits. Wallets hold real money (USD, INR). Credits are usage-based entitlements. See the credit-based-billing skill for credit management.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 20,704 | 15,225 | -26% | 1 | 1 | 0% | 2,902 | 4,260 | +47% | 0 | 0 | — |
case-02 | fail→pass | 11,506 | 13,664 | +19% | 1 | 1 | 0% | 1,813 | 3,984 | +120% | 0 | 0 | — |
case-03 | fail→pass | 16,426 | 9,544 | -42% | 1 | 1 | 0% | 3,352 | 4,726 | +41% | 0 | 0 | — |
case-04 | pass→pass | 5,384 | 9,055 | +68% | 1 | 1 | 0% | 1,091 | 3,421 | +214% | 0 | 0 | — |
case-05 | pass→pass | 16,522 | 10,029 | -39% | 1 | 1 | 0% | 2,087 | 3,406 | +63% | 0 | 0 | — |
case-06 | pass→pass | 11,788 | 4,533 | -62% | 1 | 1 | 0% | 1,394 | 3,507 | +152% | 0 | 0 | — |
case-07 | fail→pass | 14,527 | 12,261 | -16% | 1 | 1 | 0% | 1,853 | 3,762 | +103% | 0 | 0 | — |
case-08 | fail→pass | 13,876 | 5,432 | -61% | 1 | 1 | 0% | 1,672 | 3,498 | +109% | 0 | 0 | — |
case-09 | pass→pass | 12,495 | 11,796 | -6% | 1 | 1 | 0% | 2,363 | 3,678 | +56% | 0 | 0 | — |
case-10 | pass→pass | 18,536 | 8,699 | -53% | 1 | 1 | 0% | 1,973 | 3,978 | +102% | 0 | 0 | — |
case-11 | pass→pass | 16,291 | 6,456 | -60% | 1 | 1 | 0% | 2,205 | 3,648 | +65% | 0 | 0 | — |
case-12 | pass→pass | 12,550 | 9,264 | -26% | 1 | 1 | 0% | 1,440 | 3,251 | +126% | 0 | 0 | — |
case-13 | fail→pass | 12,676 | 4,264 | -66% | 1 | 1 | 0% | 1,145 | 3,331 | +191% | 0 | 0 | — |
case-14 | fail→pass | 15,562 | 12,869 | -17% | 1 | 1 | 0% | 2,485 | 4,200 | +69% | 0 | 0 | — |
case-15 | pass→pass | 14,345 | 10,065 | -30% | 1 | 1 | 0% | 1,457 | 3,552 | +144% | 0 | 0 | — |
case-16 | fail→fail | 21,192 | 17,635 | -17% | 1 | 1 | 0% | 3,174 | 5,560 | +75% | 0 | 0 | — |
case-17 | fail→pass | 15,337 | 12,685 | -17% | 1 | 1 | 0% | 2,005 | 4,565 | +128% | 0 | 0 | — |
case-18 | fail→pass | 21,823 | 15,732 | -28% | 1 | 1 | 0% | 2,624 | 4,635 | +77% | 0 | 0 | — |
case-19 | pass→pass | 13,095 | 13,649 | +4% | 1 | 1 | 0% | 1,788 | 4,225 | +136% | 0 | 0 | — |
case-20 | pass→pass | 11,490 | 2,260 | -80% | 1 | 1 | 0% | 977 | 3,087 | +216% | 0 | 0 | — |
case-21 | pass→pass | 9,107 | 9,372 | +3% | 1 | 1 | 0% | 770 | 3,296 | +328% | 0 | 0 | — |
case-22 | pass→pass | 19,540 | 13,589 | -30% | 1 | 1 | 0% | 2,647 | 4,227 | +60% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.