Examples
Metered chatbot
A chat product where every customer has their own balance and their own ceiling.
The shape most products start with: users sign up, get a small free balance, and can top up. Nobody can spend more than they have, and you can see who costs what.
Aliases, free bundle, and a router
Do this once per application. Minting keys under free only works after those names exist and are allowed on the bundle.
Create the aliases
seams models add acme/fast --source anthropic/claude-haiku-4.5
seams models add acme/smart --source anthropic/claude-sonnet-4await seams.models.add("acme/fast", {
sources: [{ provider: "anthropic", model: "claude-haiku-4.5" }],
})
await seams.models.add("acme/smart", {
sources: [{ provider: "anthropic", model: "claude-sonnet-4" }],
})from seams import models
seams.models.add(
"acme/fast",
sources=[models.AddModelSource(provider="anthropic", model="claude-haiku-4.5")],
)
seams.models.add(
"acme/smart",
sources=[models.AddModelSource(provider="anthropic", model="claude-sonnet-4")],
)Create the bundle and allow those names
seams bundles add free --name Free --markup 25%
seams bundles allow free acme/fast acme/smartawait seams.bundles.create("free", {
displayName: "Free",
markupBps: 2500,
})
await seams.bundles.allow("free", { models: ["acme/fast", "acme/smart"] })seams.bundles.create("free", display_name="Free", markup_bps=2500)
seams.bundles.allow("free", models=["acme/fast", "acme/smart"])Optional: a router over those aliases
Create the router after the targets are allowed on at least one bundle. Seams attaches it to every bundle that already allows all of its targets. Callers use the router slug the same way they use an alias.
seams routers add balanced --strategy cheapest \
--target acme/fast --target acme/smartawait seams.routers.add("balanced", {
strategy: "cheapest",
targets: [{ model: "acme/fast" }, { model: "acme/smart" }],
})seams.routers.add(
"balanced",
strategy="cheapest",
targets=[
models.AddRouterTarget(model="acme/fast"),
models.AddRouterTarget(model="acme/smart"),
],
)The chat route below calls acme/smart. You can pass balanced instead; the key’s bundle already allows both.
Wiring it up
The call path below uses the OpenAI client. Anthropic and Gemini work the same way: point their SDK at your gateway host and pass the end user's ak_. See Calling the gateway for those shapes.
On sign-up, mint a key
Do this once, when the user is created, and store key.id on your user row. The secret is shown once; keep it wherever you keep their session material, not in a log.
// app/api/signup/route.ts
import { Seams } from "@ourseams/sdk"
const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! })
export async function POST(req: Request) {
const { email } = await req.json()
const user = await db.user.create({ data: { email } })
await seams.users.create(user.id, { email, bundle: "free" })
const key = await seams.keys.mint({ endUserId: user.id, bundle: "free", name: email })
// a hard ceiling for the trial: $5.00, in micro-dollars, as a string
await seams.keys.cap(key.id, { capMicros: "5000000" })
await db.user.update({
where: { id: user.id },
data: { seamsKeyId: key.id, seamsKeySecret: encrypt(key.key) },
})
return Response.json({ id: user.id })
}# routes/signup.py
import os
from seams import Seams
seams = Seams(api_key=os.environ["SEAMS_API_KEY"])
@router.post("/signup")
async def signup(body: SignupBody):
user = await db.user.create(email=body.email)
seams.users.create(user.id, email=body.email, bundle="free")
key = seams.keys.mint(end_user_id=user.id, bundle="free", name=body.email)
# a hard ceiling for the trial: $5.00, in micro-dollars, as a string
seams.keys.cap(key.id, cap_micros="5000000")
await db.user.update(
user.id,
seams_key_id=key.id,
seams_key_secret=encrypt(key.key),
)
return {"id": user.id}On each request, call with that user's key
Your server holds the key and calls on the user's behalf. The gateway attributes the spend to them because of which key arrived, so there is nothing else to pass.
// app/api/chat/route.ts
import OpenAI from "openai"
export async function POST(req: Request) {
const user = await currentUser(req)
const client = new OpenAI({
baseURL: "https://<app>.gw.ourseams.com/v1",
apiKey: decrypt(user.seamsKeySecret),
})
try {
const stream = await client.chat.completions.create({
model: "app/smart",
messages: await history(user),
stream: true,
})
return new Response(toReadable(stream))
} catch (err) {
// 402 is the one you must handle: they are out of money or over their cap
if (err.status === 402) {
return Response.json(
{ error: "out_of_credit", topUpUrl: "https://billing.example.com/add-credit" },
{ status: 402 },
)
}
throw err
}
}# routes/chat.py
from fastapi.responses import JSONResponse, StreamingResponse
from openai import OpenAI, APIStatusError
@router.post("/chat")
async def chat(request: Request):
user = await current_user(request)
client = OpenAI(
base_url="https://<app>.gw.ourseams.com/v1",
api_key=decrypt(user.seams_key_secret),
)
try:
stream = client.chat.completions.create(
model="app/smart",
messages=await history(user),
stream=True,
)
return StreamingResponse(to_sse(stream), media_type="text/event-stream")
except APIStatusError as err:
# 402 is the one you must handle: they are out of money or over their cap
if err.status_code == 402:
return JSONResponse(
{"error": "out_of_credit", "top_up_url": "https://billing.example.com/add-credit"},
status_code=402,
)
raiseA stream that runs out mid-answer is aborted, not silently truncated. The client sees the stream end and a spend_limit_exceeded error frame, handle it the same way you handle the 402.
Show them what they spent
Either link to the hosted portal, or read the numbers yourself and render them in your own UI. Both read the same rows.
// send them to the branded page; the session is theirs, not yours
const { url } = await seams.portal.link({ externalId: user.id })
redirect(url)const { rows, totals } = await seams.usage({
endUserId: user.id,
since: startOfMonth(),
})
// rows[0] { requestedModel: "smart", calls: 1625, inputTokens: 4244640,
// outputTokens: 632760, providerCostMicros: "8230000",
// billedMicros: "11750000" }
// totals { calls: 5418, billedMicros: "39170000", … }