TypeScript SDK
The @margint-ai/sdk package runs in Node 18+, Bun, Deno, and edge runtimes that expose fetch.
npm install @margint-ai/sdk
60-second start
import { Margint } from '@margint-ai/sdk'
const m = new Margint({ apiKey: process.env.MARGINT_API_KEY! })
m.track({
customerId: 'cust_abc',
feature: 'chat',
provider: 'openai',
model: 'gpt-4o',
inputTokens: 820,
outputTokens: 310
})
// before your process exits
await m.shutdown()
Events batch in memory and flush every 5 s.
Three integration patterns
1. wrap() — zero-touch
Wrap your LLM client once and every call is tracked automatically.
import OpenAI from 'openai'
import { Margint } from '@margint-ai/sdk'
const m = new Margint({ apiKey: process.env.MARGINT_API_KEY! })
const openai = m.wrap(new OpenAI(), { customerId: 'cust_abc', feature: 'chat' })
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'hi' }]
})
Works with OpenAI and Anthropic SDK response shapes (response.usage, response.model). For other providers, use track().
2. track() — manual
For custom HTTP, multiple customers per request, or providers without a native SDK.
const res = await myLlmCall()
m.track({
customerId: req.user.id,
feature: 'summarize',
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
inputTokens: res.usage.input_tokens,
outputTokens: res.usage.output_tokens,
metadata: { requestId: req.id }
})
3. guardedCall() — budget enforcement
Blocks the call if the customer is over budget. Budget checks are cached for 60 s.
import { BudgetExceededError } from '@margint-ai/sdk'
try {
const res = await m.guardedCall(
{ customerId: 'cust_abc', feature: 'agent' },
() => openai.chat.completions.create({ model: 'gpt-4o', messages })
)
} catch (err) {
if (err instanceof BudgetExceededError) {
return reply.status(402).send({ error: 'Budget exceeded', breaches: err.breaches })
}
throw err
}
guardedCall checks the budget but does not auto-track the result — combine with wrap() or track() if you want both.
Framework quickstarts
Next.js (App Router)
Instantiate as a module-level singleton so HMR doesn't leak flush intervals.
import { Margint } from '@margint-ai/sdk'
declare global {
var __margint: Margint | undefined
}
export const margint =
globalThis.__margint ??
(globalThis.__margint = new Margint({ apiKey: process.env.MARGINT_API_KEY! }))
import { margint } from '@/lib/margint'
import OpenAI from 'openai'
export async function POST(req: Request) {
const { userId, messages } = await req.json()
const client = margint.wrap(new OpenAI(), { customerId: userId, feature: 'chat' })
const res = await client.chat.completions.create({ model: 'gpt-4o', messages })
return Response.json(res)
}
On serverless, call await margint.flush() at the end of long-lived handlers for immediate visibility. For short requests the 5 s batch timer is fine.
Nuxt 3 / 4
import { Margint } from '@margint-ai/sdk'
let instance: Margint | undefined
export function useMargint() {
return (instance ??= new Margint({ apiKey: process.env.MARGINT_API_KEY! }))
}
import OpenAI from 'openai'
import { useMargint } from '../utils/margint'
export default defineEventHandler(async (event) => {
const { userId, messages } = await readBody(event)
const client = useMargint().wrap(new OpenAI(), { customerId: userId, feature: 'chat' })
return client.chat.completions.create({ model: 'gpt-4o', messages })
})
Express
import express from 'express'
import OpenAI from 'openai'
import { Margint } from '@margint-ai/sdk'
const app = express()
const margint = new Margint({ apiKey: process.env.MARGINT_API_KEY! })
app.post('/chat', async (req, res) => {
const client = margint.wrap(new OpenAI(), { customerId: req.user.id, feature: 'chat' })
const result = await client.chat.completions.create({ model: 'gpt-4o', messages: req.body.messages })
res.json(result)
})
process.on('SIGTERM', async () => {
await margint.shutdown()
process.exit(0)
})
Configuration
new Margint({
apiKey: string, // required
endpoint?: string, // default: https://app.margint.dev/api/ingest/events
budgetEndpoint?: string, // default: https://app.margint.dev/api/budgets/check
flushIntervalMs?: number, // default: 5000
maxBatchSize?: number, // default: 50 (flushes early when reached)
budgetCacheTtlMs?: number // default: 60000
})
Troubleshooting
Events not appearing?
- Verify
apiKeymatches a key in Settings → API Keys. - Call
await m.flush()— the default 5 s timer may not fire in short scripts. - Check egress for
app.margint.dev.
Cost shows as $0?
- Model isn't in the bundled pricing database. Pass
costMicrodollarsmanually ontrack(), or email us to add the model.
wrap() doesn't track my calls?
- The response must expose
.usage(withprompt_tokens/input_tokens) and.model. If not, usetrack().