Core concepts

Spend caps

A hold before dispatch, and a stream that stops.

Before a request is dispatched, a hold is taken against the end user's balance. The invariant is available = balance − hold, computed atomically, never assembled in application code.

Set a period cap on a key

seams keys mint --user ada --bundle pro --cap 5.00 --label "Ada laptop"
seams keys cap key_01J8Q…0x2m --limit 10

Before the stream: the clamp

When the balance covers less than the max_tokens that was asked for, the request is not rejected. max_tokens is clamped down to what the balance affords, and the initial response says so.

http
HTTP/1.1 200 OK
x-request-id: req_01J...
x-seams-abort-reason: max_tokens_clamped

Read it before you read the body: the answer will be shorter than you asked for, and on a stream a mid-stream abort is possible. A clamp on its own is not an abort. It ends on the ordinary finish_reason: "length", with nothing unusual for a client to handle.

Mid-stream

While a response streams, spend is re-checked as tokens arrive. When the reservation is exhausted the connection to the model provider is closed and a terminal frame is written in the shape the inbound protocol expects.

json
{"error":{"code":"spend_limit_exceeded","reason":"reservation exhausted mid-stream"}}

A stream that simply stops is indistinguishable from a network failure, and a client that cannot tell will retry. The terminal frame is what stops that.

The lifecycle

StepWhat happens
reserveHold the worst-case cost on the balance before dispatch
streamRe-check accumulated spend every 16 chunks and every 250 ms
abortClose the model provider connection and send the terminal frame
settleRelease the hold, write the usage record, reconcile to the cent

Handling it in a client

spend_limit_exceeded is not one of the finish reasons your provider SDK knows, so an exhaustive switch will fall through to its default branch. Treat it as a normal end of stream that happens to mean the budget ran out. The response was already 200 and the usage block on the terminal frame is complete, so there is nothing to retry and nothing to reconstruct.

for await (const chunk of stream) {
  const reason = chunk.choices[0]?.finish_reason
  if (reason === "spend_limit_exceeded") {
    // the budget ran out. the answer is partial, the usage is final.
    await topUp()
    break
  }
  render(chunk.choices[0]?.delta?.content ?? "")
}

The OpenAI shapes above look for finish_reason. Anthropic and Gemini get a terminal frame in their own wire shape with the same spend_limit_exceeded code, see Calling the gateway for streaming examples on each client.

Choosing what happens when the money runs out

A bundle carries a termination policy, and it decides whether a stream is cut or allowed to finish.

seams bundles edit pro --termination hard_token
PolicyWhat the end user sees
hard_tokenThe stream stops the moment the reservation is spent. The answer is cut mid-sentence and the terminal frame carries the usage. The default, and the only one that cannot overspend.
complete_messageThe stream is never cut. The message finishes and the overspend is billed. Choose it when a truncated answer is worse for your customer than a small overrun.
graceThe stream runs past the reservation by maxOverdraftMicros and stops there. A bounded version of complete_message for when you want a ceiling on the overrun.

hard_token and grace abort at 98% of their ceiling rather than 100%, because spend is checked every 16 chunks and the unchecked chunks in between must not overshoot it.