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

Create the bundle and allow those names

seams bundles add free --name Free --markup 25%
seams bundles allow free 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/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 })
}

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

A 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)