Drip is a lightweight SDK for usage tracking and execution logging in systems where cost is tied to computation — AI agents, APIs, background jobs, and infra workloads.
This Core SDK is designed for pilots: it records what ran and how much it used, without handling billing or balances.
One line to start tracking: await drip.trackUsage({ customerId, meter, quantity })
npm install @drip-sdk/node# Secret key — full API access (server-side only, never expose publicly)
export DRIP_API_KEY=sk_test_...Or use a .env file (recommended):
npm install dotenv# .env
DRIP_API_KEY=sk_test_...Load your .env at the top of your entry file:
import 'dotenv/config';import { drip } from '@drip-sdk/node';
// Create a customer first
const customer = await drip.createCustomer({ externalCustomerId: 'user_123' });
// Track usage — that's it
await drip.trackUsage({ customerId: customer.id, meter: 'api_calls', quantity: 1 });The drip singleton reads DRIP_API_KEY from your environment automatically.
import 'dotenv/config';
import { Drip } from '@drip-sdk/node';
// Auto-reads DRIP_API_KEY from environment
const client = new Drip();
// Or pass config explicitly with an Operator/Admin secret key
const clientWithSecret = new Drip({ apiKey: 'sk_test_...' });import { drip } from '@drip-sdk/node';
async function main() {
// Verify connectivity
await drip.ping();
// Create a customer (at least one of externalCustomerId or onchainAddress required)
const customer = await drip.createCustomer({ externalCustomerId: 'user_123' });
// Record usage
await drip.trackUsage({
customerId: customer.id,
meter: 'llm_tokens',
quantity: 842,
metadata: { model: 'gpt-4o-mini' },
});
// Record an execution lifecycle
await drip.recordRun({
customerId: customer.id,
workflow: 'research-agent',
events: [
{ eventType: 'llm.call', quantity: 1700, units: 'tokens' },
{ eventType: 'tool.call', quantity: 1 },
],
status: 'COMPLETED',
});
console.log(`Customer ${customer.id}: usage + run recorded`);
}
main();Expected result:
- No errors
- Events appear in your Drip dashboard within seconds
| Concept | Description |
|---|---|
customerId |
The end user, API key, or account you're attributing usage to |
meter |
What you're measuring (tokens, requests, seconds, rows, etc.) |
quantity |
Numeric usage for that meter |
run |
A single execution or request lifecycle (success / failure / duration) |
correlationId |
Optional. Your trace/request ID for linking Drip data with your APM (OpenTelemetry, Datadog, etc.) |
Status values: PENDING | RUNNING | COMPLETED | FAILED
Event schema: Payloads are schema-flexible. Drip stores events as structured JSON and does not enforce a fixed event taxonomy.
Drip is append-only and idempotent-friendly. You can safely retry events.
Distributed tracing: Pass
correlationIdtostartRun(),recordRun(), oremitEvent()to cross-reference Drip billing with your observability stack. See FULL_SDK.md for details.
Every mutating SDK method (trackUsage, emitEvent) requires an idempotencyKey. The server uses this key to deduplicate requests — if two requests share the same key, only the first is processed. The parameter is optional in the SDK method signature because the SDK always generates one for you if you don't provide it.
Full SDK:
charge()also acceptsidempotencyKeybut is only available in the Full SDK (import { Drip } from '@drip-sdk/node'), not the Core SDK.
recordRun generates idempotency keys internally for its batch events (using externalRunId when provided, otherwise deterministic keys).
When you omit idempotencyKey, the SDK generates one automatically — this works for both the Core SDK and Full SDK. The auto key is:
- Unique per call — two separate calls with identical parameters produce different keys (a monotonic counter ensures this).
- Stable across retries — the key is generated once and reused for all retry attempts of that call, so network retries are safely deduplicated.
- Deterministic — no randomness; keys are reproducible for debugging.
This means you get free retry safety with zero configuration.
Note:
wrapApiCallgenerates a time-based key when no explicitidempotencyKeyis provided. Pass your own key if you need deterministic deduplication withwrapApiCall.
Pass your own idempotencyKey when you need application-level deduplication — e.g., to guarantee that a specific business operation is billed exactly once, even across process restarts:
const customer = await drip.createCustomer({ externalCustomerId: 'user_123' });
await drip.charge({
customerId: customer.id,
meter: 'api_calls',
quantity: 1,
idempotencyKey: `order_${orderId}_charge`, // your business-level key
});Common patterns:
order_${orderId}— one charge per orderrun_${runId}_step_${stepIndex}— one charge per pipeline stepinvoice_${invoiceId}— one charge per invoice
StreamMeter also auto-generates idempotency keys per flush. If you provide an idempotencyKey in the options, each flush appends a counter (_flush_0, _flush_1, etc.) to keep multi-flush scenarios safe.
By default, trackUsage() persists each event synchronously (mode: 'sync'). For high-throughput scenarios — thousands of events per second, sub-cent microtransactions, or fire-and-forget telemetry — use batch mode to queue events for bulk insertion:
// Sync mode (default) — waits for persistence, returns usageEventId
const sync = await drip.trackUsage({
customerId: customer.id,
meter: 'api_calls',
quantity: 1,
});
console.log(sync.usageEventId); // "evt_abc123"
// Batch mode — enqueues for bulk insert (~2s), returns immediately
const batch = await drip.trackUsage({
customerId: customer.id,
meter: 'api_calls',
quantity: 1,
mode: 'batch',
});
console.log(batch.pendingEvents); // number of queued events
console.log(batch.idempotencyKey); // key for deduplication| Mode | Latency | Response | Best for |
|---|---|---|---|
sync (default) |
~50ms | usageEventId |
Low-to-medium volume, when you need the event ID immediately |
batch |
~5ms | pendingEvents, idempotencyKey |
High-volume telemetry, sub-cent events, fire-and-forget |
The return type is a union. Use the mode field to narrow:
const result = await drip.trackUsage({
customerId: customer.id,
meter: 'tokens',
quantity: 500,
mode: 'batch',
});
if ('mode' in result && result.mode === 'batch') {
// TrackUsageBatchResult — no usageEventId yet
console.log(`Queued (${result.pendingEvents} pending)`);
} else {
// TrackUsageSyncResult — event ID available
console.log(`Tracked: ${result.usageEventId}`);
}For LLM token streaming or other scenarios where you accumulate usage incrementally, use StreamMeter to avoid per-chunk API calls:
const meter = drip.createStreamMeter({
customerId: customer.id,
meter: 'tokens',
});
for await (const chunk of llmStream) {
meter.addSync(chunk.tokens); // no API call — local accumulation
}
const result = await meter.flush(); // single API call at the endSee FULL_SDK.md for full StreamMeter options (auto-flush thresholds, callbacks, etc.).
Drip issues two key types per API key pair. Each has different access scopes:
| Key Type | Prefix | Access | Use In |
|---|---|---|---|
| Secret Key | sk_live_ / sk_test_ |
Server-side API access with RBAC | Server-side only |
| Public Key | pk_live_ / pk_test_ |
Client-safe identifier only | Not for the onboarding and billing flows in this guide |
Use a secret key for the examples in this README:
createCustomer()trackUsage()/charge()recordRun()/startRun()/emitEvent()- pricing plans, webhooks, balances, and customer management
Public keys (pk_*) are not the right credential for those flows. Depending on the method, the SDK may block the call locally with PUBLIC_KEY_NOT_ALLOWED or the API may reject it with 403 FORBIDDEN.
The SDK detects your key type automatically:
const drip = new Drip({ apiKey: 'pk_test_...' });
console.log(drip.keyType); // 'public'
// Use a secret key for customer, billing, and run/event writes:
const serverDrip = new Drip({ apiKey: 'sk_test_...' });
const customer = await serverDrip.createCustomer({ externalCustomerId: 'user_123' });
await serverDrip.trackUsage({ customerId: customer.id, meter: 'api_calls', quantity: 1 });| Variant | Description |
|---|---|
| Core SDK (recommended for pilots) | Usage tracking + execution logging only |
| Full SDK | Includes billing, balances, and workflows (for later stages) |
Available via import { drip } from '@drip-sdk/node/core' (tracking + runs only, no billing):
| Method | Description |
|---|---|
ping() |
Verify API connection |
createCustomer(params) |
Create a customer |
getCustomer(customerId) |
Get customer details |
listCustomers(options) |
List all customers |
trackUsage(params) |
Record metered usage (non-billing, hits /usage/internal) |
recordRun(params) |
Log complete agent run (simplified) |
startRun(params) |
Start execution trace (requires workflowId — use recordRun for simpler flow) |
emitEvent(params) |
Log event within run |
emitEventsBatch(params) |
Batch log events |
endRun(runId, params) |
Complete execution trace |
getRun(runId) |
Get run details and summary |
getRunTimeline(runId) |
Get execution timeline |
checkEntitlement(params) |
Pre-request authorization check |
| Method | Description |
|---|---|
getOrCreateCustomer(externalCustomerId, metadata?) |
Idempotently create or retrieve a customer by external ID |
charge(params) |
Create a billable charge directly (requires pricing plan) |
chargeAsync(params) |
Async charge — returns 202 immediately, processes in background |
listCharges(options?) |
List charges for your business |
getCharge(chargeId) |
Get a single charge by ID |
getBalance(customerId) |
Get customer balance |
listEvents(options?) |
List execution events with filters |
getEvent(eventId) |
Get a single event by ID |
getEventTrace(eventId) |
Get event causality trace (ancestors, children, retries) |
wrapApiCall(params) |
Wrap external API call with guaranteed usage recording |
checkout(params) |
Create hosted checkout session for top-ups |
All parameters are optional, but at least one of externalCustomerId or onchainAddress must be provided:
// Simplest — just your internal user ID
const customer = await drip.createCustomer({ externalCustomerId: 'user_123' });
// With an on-chain address (for on-chain billing)
const withAddress = await drip.createCustomer({
onchainAddress: '0x1234...',
externalCustomerId: 'user_123',
});
// Internal/non-billing customer (for tracking only)
const internal = await drip.createCustomer({
externalCustomerId: 'internal-team',
isInternal: true,
});| Parameter | Type | Required | Description |
|---|---|---|---|
externalCustomerId |
string |
No* | Your internal user/account ID |
onchainAddress |
string |
No* | Customer's Ethereum address |
isInternal |
boolean |
No | Mark as internal (non-billing). Default: false |
metadata |
object |
No | Arbitrary key-value metadata |
*At least one of externalCustomerId or onchainAddress is required.
- AI agents (token metering, tool calls, execution traces)
- API companies (per-request billing, endpoint attribution)
- RPC providers (multi-chain call tracking)
- Cloud/infra (compute seconds, storage, bandwidth)
For billing, entitlements, subscriptions, invoices, contracts, webhooks, middleware, and advanced features:
import { Drip } from '@drip-sdk/node';
const drip = new Drip({ apiKey: 'sk_live_...' });
// Charge a customer for usage
const result = await drip.charge({
customerId: customer.id,
meter: 'api_calls',
quantity: 100,
});
console.log(`Charged ${result.charge.amountUsdc} USDC`);
// Check if a customer can use a feature before processing
const check = await drip.checkEntitlement({
customerId: customer.id,
featureKey: 'search',
});
if (!check.allowed) {
// Over quota — return 429 without wasting compute
}Key methods:
| Method | Description |
|---|---|
getBalance(customerId) |
Get customer balance (USDC, pending, available) |
checkEntitlement(params) |
Pre-request authorization check (allowed/denied + remaining quota) |
setCustomerSpendingCap(customerId, params) |
Set daily/monthly/single-charge spending cap |
getCustomerSpendingCaps(customerId) |
List active spending caps |
removeCustomerSpendingCap(customerId, capId) |
Remove a spending cap |
checkout(params) |
Create hosted checkout session for top-ups |
Highlights:
- Billing —
charge(),listCharges(),getCharge(),getBalance() - Cost Estimation —
estimateFromUsage(),estimateFromHypothetical()for budget planning - Spending Caps — per-customer daily/monthly limits with multi-level alerts at 50%, 80%, 95%, 100%
- Entitlements — pre-request quota gating with
checkEntitlement() - Subscription billing — create, update, pause, resume, cancel
- Invoices — available via REST API (SDK methods planned)
- Contracts — available via REST API (SDK methods planned)
- Webhooks — create, verify, manage webhook endpoints
- Middleware — Next.js and Express integrations
See FULL_SDK.md for complete documentation.
Give your customers a read-only dashboard showing their balance, charges, session keys, and settlements — no wallet connection needed.
How it works: You create a short-lived portal session. You get back a URL. Send it to your customer (email, in-app link, etc.). They open it and see their data. The link expires automatically.
import { Drip } from '@drip-sdk/node';
const drip = new Drip({ apiKey: 'sk_live_...' }); // requires secret key
// 1. Create a portal link (default: expires in 60 minutes)
const session = await drip.createPortalSession({
customerId: 'cust_abc123',
expiresInMinutes: 120, // optional, 5–1440 (24h max)
});
// 2. Send the URL to your customer
const portalUrl = `https://app.drippay.dev${session.url}`;
// → "https://app.drippay.dev/portal/abc..."
// 3. Revoke early if needed (optional)
await drip.revokePortalSession(session.id);| Method | Description |
|---|---|
createPortalSession(params) |
Create a portal link for a customer |
revokePortalSession(sessionId) |
Revoke a portal link immediately |
What the customer sees:
- Account info (address, status)
- Balance, pending charges, total spent
- Recent transactions
- Session keys (read-only)
- Charge + usage event history
- Settlement history with on-chain tx links
Both methods require a secret key (sk_). Portal tokens are read-only — customers can view but not modify anything.
import { Drip, DripError } from '@drip-sdk/node';
try {
await drip.trackUsage({ ... });
} catch (error) {
if (error instanceof DripError) {
console.error(`Error: ${error.message} (${error.code})`);
}
}- Node.js 18.0.0 or higher (SDK supports Node 18+; the Drip monorepo uses Node 24.x)