# seams documentation > Gateway, routing, metering, spend control, and BYOK, how to reach them. > Human site: https://docs.ourseams.com ## Get started - [Welcome](https://docs.ourseams.com/welcome): The portal for your AI product: gateway, routing, metering, and spend control. - [Introduction](https://docs.ourseams.com/introduction): The shape of the system, and where your code touches it. - [Quickstart](https://docs.ourseams.com/quickstart): Mint a key for one end user and make a real call through your gateway. - [Authentication](https://docs.ourseams.com/authentication): Two kinds of key, pointed at two different things, and never interchangeable. ## Core concepts - [Applications](https://docs.ourseams.com/applications): The container everything else belongs to, and the two hostnames it gets. - [Keys and end users](https://docs.ourseams.com/keys-and-end-users): One key per end user, and why never a header. - [Wallets and holds](https://docs.ourseams.com/balances-and-holds): Where the money sits, and what a hold actually is. - [The ledger](https://docs.ourseams.com/the-ledger): Append-only, and why that is not negotiable. - [Bundles](https://docs.ourseams.com/bundles): Which models a key may reach, and at what markup. - [Pricing strategies](https://docs.ourseams.com/pricing-strategies): How you charge end users maps onto wallets, plans, markup, and job ceilings. - [Subscriptions and overage](https://docs.ourseams.com/subscriptions): What a paid bundle mints each period, and what happens when it runs out. - [Credit systems](https://docs.ourseams.com/credit-systems): Name your own credits, decide when they mint, and let the gateway spend them down. - [Spend caps](https://docs.ourseams.com/spend-caps): A hold before dispatch, and a stream that stops. - [Outcomes](https://docs.ourseams.com/outcomes): Budget a unit of work, not a request, when one job is several calls. - [Model aliases](https://docs.ourseams.com/model-aliases): Your names for models, not the provider's. - [Routers](https://docs.ourseams.com/routers): A named policy over your model names, when one source is not enough. - [Provider accounts](https://docs.ourseams.com/providers): Your provider accounts, what we do with them, and the catalog they draw from. - [Bring your own key](https://docs.ourseams.com/byok): Your provider account, sealed with KMS. - [Emitters](https://docs.ourseams.com/emitters): Settled usage, delivered where you bill from. ## Integrations - [Agent onboarding](https://docs.ourseams.com/agent-onboarding): An agent that provisions its own key, its own budget and its own base URL, no console visit. - [CLI](https://docs.ourseams.com/cli): Mint keys, read what spend went where, and check a broken integration without opening the console. ## SDKs - [TypeScript SDK](https://docs.ourseams.com/typescript-sdk): @ourseams/sdk. one namespace per CLI command group. - [Python SDK](https://docs.ourseams.com/python-sdk): ourseams-sdk. the same operations, imported as seams. ## Surfaces - [The gateway](https://docs.ourseams.com/the-gateway): Your gateway host: four wire formats, model resolution, and the only path that moves money. - [Calling the gateway](https://docs.ourseams.com/calling-the-gateway): OpenAI, Anthropic, Gemini, and curl: non-streaming, streaming, and cross-protocol. - [The portal](https://docs.ourseams.com/the-portal): Where your customers see their own usage, spend and keys. - [The console](https://docs.ourseams.com/the-console): Where your team runs the account. ## Examples - [Metered chatbot](https://docs.ourseams.com/example-metered-chatbot): A chat product where every customer has their own balance and their own ceiling. - [Usage-based pricing](https://docs.ourseams.com/example-usage-based-pricing): Charge for what they call: your markup on the wire, their wallet on the meter. - [Outcome-based pricing](https://docs.ourseams.com/example-outcome-based-pricing): Sell a finished job. Budget the run. Meter every call that belongs to it. - [Design a credit system](https://docs.ourseams.com/example-credit-system): Welcome credit, sellable packs, and a monthly allotment, one product, end to end. - [Per-seat agents](https://docs.ourseams.com/example-per-seat-agents): One customer, many agents, and a budget that does not let one of them eat the month. - [Prepaid credits](https://docs.ourseams.com/example-prepaid-credits): Sell a balance up front, take your margin at the top-up, and let the meter run it down. - [Reselling with markup](https://docs.ourseams.com/example-reselling): Your own provider account on the path, your own price on the invoice. ## Reference - [API reference](https://docs.ourseams.com/api-reference): The OpenAPI document is the list, generated from the routes that are served. - [Every operation](https://docs.ourseams.com/operations): The command and the route that reach each operation. - [Usage and margin](https://docs.ourseams.com/usage-and-margin): What was called, what it cost you, and what was left over. - [The request log](https://docs.ourseams.com/request-log): One row per call, why it cost what it cost, and what is deliberately absent. - [Errors](https://docs.ourseams.com/errors): Stable codes, and what each one means. - [Lossy protocol conversions](https://docs.ourseams.com/protocol-lossy-conversions): What is not preserved when inbound and outbound wires differ. - [FAQ](https://docs.ourseams.com/faq): The questions that come up first. - [Changelog](https://docs.ourseams.com/changelog): What shipped, newest first. ## Read instead of crawling - [https://docs.ourseams.com/llms-full.txt](https://docs.ourseams.com/llms-full.txt): every docs page, one file - [https://api.ourseams.com/v1/openapi.json](https://api.ourseams.com/v1/openapi.json): every endpoint, field and error - [https://ourseams.com/llms.txt](https://ourseams.com/llms.txt): product index - [https://blog.ourseams.com/llms.txt](https://blog.ourseams.com/llms.txt): blog index --- # Welcome > The portal for your AI product: gateway, routing, metering, and spend control. > Section: Get started · [https://docs.ourseams.com/welcome](https://docs.ourseams.com/welcome) Seams sits between your product and the model providers. Your customers call your gateway under names you choose. Bundles decide what they can reach, routers decide how those names resolve, and every request is metered against a balance with a cap that holds for the life of the stream. They get a usage page with your name on it. Point your OpenAI, Anthropic, or Gemini client at your application gateway, mint one `ak_` key per end user, and metering happens on the request path. ## Start here - **[Introduction](https://docs.ourseams.com/introduction)**: What sits where, and which problem each piece solves. - **[Quickstart](https://docs.ourseams.com/quickstart)**: Mint a key and make a real call through your gateway. - **[Calling the gateway](https://docs.ourseams.com/calling-the-gateway)**: OpenAI, Anthropic, Gemini, and curl: including streaming and cross-protocol. - **[Core concepts](https://docs.ourseams.com/balances-and-holds)**: Balances, holds, aliases, and caps: the words used across these guides. - **[Credit systems](https://docs.ourseams.com/credit-systems)**: Design packs, plan allotments, and what one credit means in your product. - **[Pricing strategies](https://docs.ourseams.com/pricing-strategies)**: Usage-based, prepaid, plans, outcomes: which nouns to use. - **[Outcomes](https://docs.ourseams.com/outcomes)**: Budget a job, not a request, when one unit of work is several calls. - **[Examples](https://docs.ourseams.com/example-metered-chatbot)**: Full integrations you can read end to end and adapt. ## What it replaces | You would otherwise write | What Seams does | | --- | --- | | A proxy and a redeploy every time a model SKU changes | Aliases and routers you change in the console | | A per-user token counter and a cron to reconcile it | Usage recorded per request, per key, per model | | A balance column and a race you hope never happens | `available = balance − hold`, atomic | | A cap check before the call that is wrong mid-stream | A reservation that stops the stream at the limit | | Provider keys in your env, shared by everyone | One credential per customer, decrypted per request | | A billing page you keep meaning to build | A branded portal on your subdomain | > Seams never stores prompts or completions. The usage record has no place for message content. That is by design, not a setting. ## Need a hand - **[FAQ](https://docs.ourseams.com/faq)**: Questions that come up in the first week. - **[Error reference](https://docs.ourseams.com/errors)**: Client-facing error codes, and what to do next. --- # Introduction > The shape of the system, and where your code touches it. > Section: Get started · [https://docs.ourseams.com/introduction](https://docs.ourseams.com/introduction) There are three surfaces. The **gateway** is the inference path: authenticate the key, resolve the model, meter and reserve spend, call the provider, settle. The **console** is where your team configures models, routers, bundles, and keys. The **portal** is where your customers see usage and limits. ```text your app seams provider ───────── ───── ──────── OpenAI · Anthropic · Gemini Authorization: ak_acme_… → resolve key → end user check bundle → allowed model reserve worst-case cost → provider meter the stream ← tokens settle: hold → charge ← 200 + usage ``` ## The four things you configure | Thing | What it decides | Where | | --- | --- | --- | | Bundle | Which models, at what markup, under what cap | Console → Bundles | | Alias | The model name your users type | Console → Models | | Router | How that name resolves across providers | Console → Routers | | Credential | Whose provider key pays for the call | Console → Providers | | Key | Which end user a request belongs to | `POST /v1/keys` | ## Who a call belongs to One end-user key (`ak_`) per person. That key is what ties spend to the right balance and portal account. Attribution lives on the credential, not in a request header. > Sharing one key across many people makes usage harder to break down later. Mint per end user when you can. ## Money is integers Every amount is a `bigint` of micro-dollars, a millionth of a dollar. Columns end in `_micros` and the API returns them that way. `$3.00` per million tokens is `3000000`. There is no float anywhere on the money path, and no rounding until the moment something is displayed. --- # Quickstart > Mint a key for one end user and make a real call through your gateway. > Section: Get started · [https://docs.ourseams.com/quickstart](https://docs.ourseams.com/quickstart) You need a customer token (`sk_`) from the console. Everything below is one end user, one key, one call, the smallest thing that produces a real number on a real page. ## The whole thing, in commands Every step below, with no language to choose. `seams` is the CLI (`npm i -g @ourseams/cli`); everything it does is also an HTTP call, and the same run in `curl` is on the second tab. #### cli ```bash export SEAMS_API_KEY="sk_acme_…" seams models add acme/smart --source anthropic/claude-sonnet-4 seams bundles add pro --name Pro seams bundles allow pro acme/smart # mint a key for one end user; the secret is in this response and nowhere else seams keys mint --user alice --bundle pro --cap 50 --expires 1h # your application's own hostnames, as full urls seams apps show acme # call the gateway with the end user's ak_ curl https://acme.gw.ourseams.com/v1/chat/completions \ -H "Authorization: Bearer $END_USER_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "acme/smart", "messages": [{ "role": "user", "content": "Say hello" }] }' # what it cost seams usage --user alice ``` #### curl ```bash curl -X POST https://api.ourseams.com/v1/keys \ -H "Authorization: Bearer $SEAMS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endUserId": "alice", "email": "alice@firm.com", "bundle": "pro", "capMicros": "50000000", "expiresAt": "2099-01-01T00:00:00Z" }' # { "id": "key_…", "key": "ak_acme_…", "keyPrefix": "ak_acme_a1b2c3", # "endUserId": "alice", "capMicros": "50000000", "expiresAt": "2099-01-01T00:00:00Z" } ``` > Keep what mint gives you back: the `id` over the API, the `keyPrefix` the CLI prints. `keys show` takes either. An end-user id only works while that person holds exactly one key. ## Step by step ### 1. Store your customer token The `sk_` token authenticates your backend to the Seams API. It mints end-user keys and reads usage. It is not the key your users hold. ```bash export SEAMS_API_KEY="sk_acme_…" ``` > Never ship an `sk_` to a browser or a mobile app. It can mint keys and read every customer's spend. ### 2. Name a model and allow it on a bundle Create your alias, then allow it on the bundle you will mint under. Skip `bundles add` if onboarding already created `pro`. #### cli ```bash seams models add acme/smart --source anthropic/claude-sonnet-4 seams bundles add pro --name Pro seams bundles allow pro acme/smart ``` #### typescript ```ts await seams.models.add("acme/smart", { sources: [{ provider: "anthropic", model: "claude-sonnet-4" }], }) await seams.bundles.create("pro", { displayName: "Pro" }) await seams.bundles.allow("pro", { models: ["acme/smart"] }) ``` #### python ```python from seams import models seams.models.add( "acme/smart", sources=[models.AddModelSource(provider="anthropic", model="claude-sonnet-4")], ) seams.bundles.create("pro", display_name="Pro") seams.bundles.allow("pro", models=["acme/smart"]) ``` > A router is optional. After its targets are on a bundle, `seams routers add` attaches it. See Routers. ### 3. Mint a key for one end user Mint under your own identifier. If that person does not exist yet, mint creates them and opens their balance. #### cli ```bash curl https://api.ourseams.com/v1/keys \ -H "Authorization: Bearer $SEAMS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endUserId": "alice", "email": "alice@firm.com", "bundle": "pro", "capMicros": "50000000" }' ``` #### typescript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const key = await seams.keys.mint({ endUserId: "alice", email: "alice@firm.com", bundle: "pro", capMicros: "50000000", }) // secret is shown once console.log(key.id, key.key) ``` #### python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) key = seams.keys.mint( end_user_id="alice", email="alice@firm.com", bundle="pro", cap_micros="50000000", ) # secret is shown once print(key.id, key.key) ``` > The secret is never retrievable again. Save it before you do anything else. ### 4. Point the client at your gateway Your application has its own gateway host. It speaks the OpenAI, Anthropic, and Gemini wire formats, so the client you already use works unchanged. Point that host as the base URL, and pass the end user's `ak_` instead of your provider key. #### openai ```ts import OpenAI from "openai" const client = new OpenAI({ baseURL: "https://.gw.ourseams.com/v1", apiKey: endUserKey, }) const res = await client.chat.completions.create({ model: "acme/smart", messages: [{ role: "user", content: "Say hello" }], }) ``` #### anthropic ```ts import Anthropic from "@anthropic-ai/sdk" const client = new Anthropic({ baseURL: "https://.gw.ourseams.com", apiKey: endUserKey, }) const res = await client.messages.create({ model: "acme/smart", max_tokens: 1024, messages: [{ role: "user", content: "Say hello" }], }) ``` #### gemini ```ts import { GoogleGenAI } from "@google/genai" const client = new GoogleGenAI({ apiKey: endUserKey, httpOptions: { baseUrl: "https://.gw.ourseams.com" }, }) const res = await client.models.generateContent({ model: "acme/smart", contents: "Say hello", }) ``` #### curl ```bash # openai.chat curl https://.gw.ourseams.com/v1/chat/completions \ -H "Authorization: Bearer $END_USER_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "acme/smart", "messages": [{ "role": "user", "content": "Say hello" }] }' # anthropic.messages curl https://.gw.ourseams.com/v1/messages \ -H "Authorization: Bearer $END_USER_KEY" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "acme/smart", "max_tokens": 1024, "messages": [{ "role": "user", "content": "Say hello" }] }' # google.generate curl "https://.gw.ourseams.com/v1beta/models/acme%2Fsmart:generateContent" \ -H "Authorization: Bearer $END_USER_KEY" \ -H "Content-Type: application/json" \ -d '{ "contents": [{ "parts": [{ "text": "Say hello" }] }] }' ``` ### 5. Read what it cost Usage is queryable immediately, and the same number is on the customer's portal page. #### cli ```bash curl "https://api.ourseams.com/v1/usage?endUserId=alice" \ -H "Authorization: Bearer $SEAMS_API_KEY" ``` #### typescript ```ts const { totals } = await seams.usage({ endUserId: "alice" }) console.log(totals.calls, totals.billedMicros) ``` #### python ```python usage = seams.usage(end_user_id="alice") print(usage.totals.calls, usage.totals.billed_micros) ``` ## What you just built - An alias on a bundle, and a key that may call it - A metered call with a usage row and portal spend under your brand ## Where to go next - **[Calling the gateway](https://docs.ourseams.com/calling-the-gateway)**: Streaming, curl, and cross-protocol calls. - **[Model aliases](https://docs.ourseams.com/model-aliases)**: Create names and swap the source later. - **[Routers](https://docs.ourseams.com/routers)**: Policy over aliases; attaches when targets are allowed. - **[Spend caps](https://docs.ourseams.com/spend-caps)**: Hard ceilings and what callers see when they hit them. --- # Authentication > Two kinds of key, pointed at two different things, and never interchangeable. > Section: Get started · [https://docs.ourseams.com/authentication](https://docs.ourseams.com/authentication) | | `sk_` customer token | `ak_` end-user key | | --- | --- | --- | | Held by | Your backend | Your customer's client, or your server on their behalf | | Reaches | `api.ourseams.com` | `.gw.ourseams.com` | | Can | Mint keys, read every end user's spend | Make one call through the gateway | | Bound to | An organization | One end user and their balance | | If it leaks | Rotate immediately, it can mint | Revoke that one key | > Never ship an `sk_` to a browser, a mobile app or an agent you do not control. It can mint keys against any of your end users and read what all of them spent. ## Getting a customer token Tokens are minted, listed and revoked like anything else. Give each one a label, because a token you cannot identify is a token nobody will ever dare revoke. ```bash seams tokens create --label ci # key sk_acme_… # key prefix sk_acme_ci0000 # last four 9f2c # warning shown once; store it now ``` ```bash seams tokens list seams tokens revoke apk_01JQZ8N4KP00000000000CI --yes ``` ## Where the CLI keeps yours `seams auth login` writes the token to `~/.seams/config.json`. In CI, put it in an environment variable instead and read it back where a command needs it. ```bash seams auth login --token "$SEAMS_API_KEY" seams auth token # prints it, for $(seams auth token) seams auth logout # forgets it ``` > The SDKs take the token as a constructor argument and never read a file. `new Seams({ apiKey })` in TypeScript, `Seams(api_key=…)` in Python. --- # Applications > The container everything else belongs to, and the two hostnames it gets. > Section: Core concepts · [https://docs.ourseams.com/applications](https://docs.ourseams.com/applications) An application is a single product. It owns its end users, its keys, its bundles and its model names, and it is the thing a base URL points at. A customer with two products has two applications and no shared end users between them. ## Create one Creating an application assigns both hostnames at once. You do not configure DNS, and you do not choose the shape. #### CLI ```bash seams apps create acmechat --name "Acme Chat" # slug acmechat # gateway https://acmechat.gw.ourseams.com/v1 # portal https://acmechat.portal.ourseams.com ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const app = await seams.apps.create("acmechat", { name: "Acme Chat" }) // app.gateway · app.portal ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) app = seams.apps.create("acmechat", name="Acme Chat") # app.gateway · app.portal ``` Clients point at that gateway hostname. The slug in the Host header is how the call finds this application; there is no default app. ```bash curl https://acmechat.gw.ourseams.com/v1/chat/completions \ -H "Authorization: Bearer ak_acmechat_…" \ -H "Content-Type: application/json" \ -d '{ "model": "acme/smart", "messages": [{ "role": "user", "content": "Say hello" }] }' ``` > Custom domains are not offered. Every surface is a subdomain of ours, which keeps certificate issuance, application resolution and the portal's cookie scope out of your operations. ## Switch and inspect `use` records the active application for later calls. Locally the CLI also writes `~/.seams/config.json`; the API `use` is what the SDKs share. #### CLI ```bash seams apps list seams apps use acme-chat seams apps current seams apps show acme-chat ``` #### TypeScript ```ts await seams.apps.list() await seams.apps.use("acme-chat") const current = await seams.apps.current() const one = await seams.apps.show("acme-chat") ``` #### Python ```python seams.apps.list() seams.apps.use("acme-chat") current = seams.apps.current() one = seams.apps.show("acme-chat") ``` > A `sk_` token is scoped to its organization, not to the active application. Switching changes which application your commands read; it does not widen or narrow the token. ## Archive #### CLI ```bash seams apps rm acme-chat ``` #### TypeScript ```ts await seams.apps.rm("acme-chat") ``` #### Python ```python seams.apps.rm("acme-chat") ``` `rm` archives rather than deletes. Usage rows and ledger entries outlive the application they were written under, because an audit trail that can be removed is not one. ## What one holds | Field | Means | | --- | --- | | `slug` | The name in both hostnames; immutable once created | | `gateway` | Your gateway host (what clients call) | | `portal` | Where an end user lands from a portal session | | `endUsers` · `keys` | How many of each the application has | | `bundles` · `models` | What is on offer and what it resolves to | | `costUsd` | Provider cost attributed to this application | --- # Keys and end users > One key per end user, and why never a header. > Section: Core concepts · [https://docs.ourseams.com/keys-and-end-users](https://docs.ourseams.com/keys-and-end-users) Attribution lives on the credential, not in the request. A key is minted for one end user and bound to their balance at that moment, so the caller never asserts who they are. ## Why not a header A header is an assertion. Anything holding the key could set it to anything, and your usage records would be a record of what callers claimed rather than what happened. ## Create an end user and mint a key #### CLI ```bash seams users create ada --email ada@firm.com --bundle pro seams keys mint --user ada --bundle pro --cap 5.00 --label "Ada laptop" # ak_acme_m8avt…SIrY # warning shown once; store it now ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) await seams.users.create("ada", { email: "ada@firm.com", bundle: "pro" }) const key = await seams.keys.mint({ endUserId: "ada", bundle: "pro", capMicros: "5000000", name: "Ada laptop", }) // key.key is shown once ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) seams.users.create("ada", email="ada@firm.com", bundle="pro") key = seams.keys.mint( end_user_id="ada", bundle="pro", cap_micros="5000000", name="Ada laptop", ) # key.key is shown once ``` ## Read and change the ceiling #### CLI ```bash seams keys show key_01J8Q…0x2m seams keys cap key_01J8Q…0x2m --limit 10 seams users show ada ``` #### TypeScript ```ts const shown = await seams.keys.show(key.id) await seams.keys.cap(key.id, { capMicros: "10000000" }) const user = await seams.users.show("ada") ``` #### Python ```python shown = seams.keys.show(key.id) seams.keys.cap(key.id, cap_micros="10000000") user = seams.users.show("ada") ``` ## Freeze and revoke #### CLI ```bash seams users freeze ada seams users unfreeze ada seams keys revoke key_01J8Q…0x2m --yes ``` #### TypeScript ```ts await seams.users.freeze("ada") await seams.users.unfreeze("ada") await seams.keys.revoke(key.id) ``` #### Python ```python seams.users.freeze("ada") seams.users.unfreeze("ada") seams.keys.revoke(key.id) ``` ## Key shape | Field | Meaning | | --- | --- | | `keyPrefix` | `ak__`, the leader, safe to display | | `lastFour` | The last four characters, for recognition | | `revokedAt` | Set on revocation; the row stays as evidence | > Revoked keys are still listed. A key that vanishes on revocation takes the evidence with it. --- # Wallets and holds > Where the money sits, and what a hold actually is. > Section: Core concepts · [https://docs.ourseams.com/balances-and-holds](https://docs.ourseams.com/balances-and-holds) Every end user has one balance per mode. A balance has a balance and a reserved amount, and what a call may spend is the difference between them. ```text available = balance − reserved ``` ## Inspect a balance #### CLI ```bash seams users show ada # external id ada # bundle pro # available $4.97 # reserved $0.00 ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const user = await seams.users.show("ada") // user.balance.balanceUnits · user.balance.reservedUnits ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) user = seams.users.show("ada") # user.balance.balance_units · user.balance.reserved_units ``` ## Credit the balance A grant lands immediately on the money wallet and writes a ledger entry. Always micros, the CLI takes dollars; the SDKs take micro-dollars as a string (`5000000` is $5.00). It does not mint named plan credits; those come from a bundle include. See Credit systems. #### CLI ```bash seams users grant 5.00 --user ada --reason "trial top-up" # granted $5.00 → ada · available $9.97 ``` #### TypeScript ```ts await seams.grants.issue({ endUsers: ["ada"], amountUnits: "5000000", reason: "trial top-up", }) ``` #### Python ```python seams.grants.issue( end_users=["ada"], amount_units="5000000", reason="trial top-up", ) ``` ## Why a hold and not a debit A streaming call's final cost is unknown when it starts. Debiting the worst case up front would show your customer a balance that is wrong for the length of the request; holding it shows them a balance that is honest and an amount that is pending. | Movement | When | Effect | | --- | --- | --- | | `reserve` | Before dispatch | `reserved` goes up, `balance` unchanged | | `settle` | On completion | `balance` goes down by actual cost, `reserved` released | | `release` | On failure | `reserved` goes down, `balance` untouched | > Both movements happen in one transaction. A hold released in a second transaction can be released twice, or not at all. --- # The ledger > Append-only, and why that is not negotiable. > Section: Core concepts · [https://docs.ourseams.com/the-ledger](https://docs.ourseams.com/the-ledger) Every movement of money writes a `ledger_entry`. Entries are never updated and never deleted, a database trigger refuses both, so the guarantee survives a migration, a psql session, and a future role misconfiguration. ## Read one end user's trail #### CLI ```bash seams users ledger ada # TYPE AMOUNT USD BALANCE AFTER AT # grant 5.00 5.00 2026-09-01T10:02:11Z # usage_debit -0.03 4.97 2026-09-01T10:04:02Z # usage_debit -0.01 4.96 2026-09-01T10:04:18Z ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const { data } = await seams.users.ledger("ada") for (const entry of data) { console.log(entry.kind, entry.amountUnits, entry.balanceAfterUnits) } ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) ledger = seams.users.ledger("ada") for entry in ledger.data: print(entry.kind, entry.amount_units, entry.balance_after_units) ``` ## Entry types | Type | Means | | --- | --- | | `topup` | The end user paid and their balance was credited | | `grant` | Credit issued without a payment, a trial, a support gesture | | `usage_debit` | Inference consumed, at raw provider cost | | `refund` | Money returned | | `reversal` | A top-up undone, a chargeback, or a duplicate | | `adjustment` | A correction, including credit from an orphaned hold | > Idempotency is a unique index, never application logic. A check-then-insert has a race window; a constraint does not. --- # Bundles > Which models a key may reach, and at what markup. > Section: Core concepts · [https://docs.ourseams.com/bundles](https://docs.ourseams.com/bundles) Every key is minted under a bundle. The bundle decides which aliases that key can reach and what margin sits on top of provider cost. ## Create a tier #### CLI ```bash seams bundles add pro --name Pro --price 29/month # slug pro # price $29.00/month ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) await seams.bundles.create("pro", { displayName: "Pro", priceMicros: "29000000", interval: "month", }) ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) seams.bundles.create( "pro", display_name="Pro", price_micros="29000000", interval="month", ) ``` ## Allow models and set a spend limit #### CLI ```bash seams bundles allow pro app/fast app/smart seams bundles edit pro --spend-limit 50/month seams bundles show pro ``` #### TypeScript ```ts await seams.bundles.allow("pro", { models: ["app/fast", "app/smart"] }) await seams.bundles.edit("pro", { spendLimitMicros: "50000000", spendLimitPeriod: "month", }) await seams.bundles.show("pro") ``` #### Python ```python seams.bundles.allow("pro", models=["app/fast", "app/smart"]) seams.bundles.edit( "pro", spend_limit_micros="50000000", spend_limit_period="month", ) seams.bundles.show("pro") ``` ## Deny a model or remove the bundle #### CLI ```bash seams bundles deny pro app/vision seams bundles rm pro --yes ``` #### TypeScript ```ts await seams.bundles.deny("pro", { models: ["app/vision"] }) await seams.bundles.rm("pro") ``` #### Python ```python seams.bundles.deny("pro", models=["app/vision"]) seams.bundles.rm("pro") ``` ## Alias matching Three pattern forms, and never a regex: an exact alias, a prefix with a trailing wildcard, or a bare wildcard for everything the customer owns. ```text app/smart exact app/* every alias in this namespace * everything ``` > Write-time validation, so a request only does a plain lookup. A bundle that names a model that does not exist is refused when it is saved, not when a request arrives. ## Routers ride along Allow aliases on the bundle, not router names. After those aliases are allowed, creating a router whose targets are all on this bundle attaches the router here automatically. Callers then use the router slug or any allowed alias. See Routers for the create step. ## Pricing lives here too Markup and sell prices on the bundle are how usage-based margin is set. Subscription price and includes are how plans mint allotments. For the full map of strategies, usage, prepaid, plans, outcomes, resell, see Pricing strategies. - **[Model aliases](https://docs.ourseams.com/model-aliases)**: Create the names you allow on a bundle. - **[Routers](https://docs.ourseams.com/routers)**: Attach a policy after the targets are allowed. - **[Pricing strategies](https://docs.ourseams.com/pricing-strategies)**: Which Seams nouns match how you charge. - **[Subscriptions and overage](https://docs.ourseams.com/subscriptions)**: Recurring grants and then-on-demand. --- # Pricing strategies > How you charge end users maps onto wallets, plans, markup, and job ceilings. > Section: Core concepts · [https://docs.ourseams.com/pricing-strategies](https://docs.ourseams.com/pricing-strategies) Seams meters every call in money. How you present that to end users is a product choice: pay as they go, prepaid packs, a monthly allotment, a price per finished job, or a mix. Use this map to pick the nouns, then follow the linked pages and examples for the wiring. ## What you are choosing | Question | What it decides | | --- | --- | | When money moves | Before use (prepaid), with use (metered), or with a finished job (outcome) | | What they see | Dollars, named credits, seats, or “N replies / month” | | Where margin lands | On each call (markup / sell price), at top-up, or in the plan price | | What stops a runaway | Wallet empty, key / bundle spend cap, or an outcome ceiling on one job | ## Strategies that work today | Strategy | End user pays for | Seams nouns | Start here | | --- | --- | --- | --- | | Usage-based | What they actually called, at your price | Bundle markup or sell price · money wallet · optional key cap | Usage-based pricing | | Prepaid credits | Balance bought up front | `grants.issue` (always micros) · markup at top-up | Prepaid credits | | Plan + allotment | A subscription that mints allowance each period | `bundles include` · entitlement before wallet | Credit systems · Subscriptions | | Outcome-based | A finished job (reply, summary, agent run) | `outcomes` ceiling · attribute `outcome` / `outcome_id` on calls | Outcome-based pricing | | Resell with margin | Your rate on your provider credential | BYOK account · markup / sell price · margin reads | Reselling with markup | | Per-seat / per-agent | A budget per agent or seat, not one shared pool | One end user + key + cap per agent | Per-seat agents | > You can combine them. A Pro plan can mint monthly credits, allow wallet top-ups when the pool runs out, and still put an outcome ceiling on each support ticket. ## Usage-based No pack vocabulary required. Allow models on a bundle, set markup or a fixed sell price per million tokens, mint a key, and let calls debit the money wallet. Empty wallet → `insufficient_credit`. Use a key or bundle spend cap so one loop cannot empty the account overnight. ## Prepaid and plan credits Prepaid top-ups always land on the money wallet in micros, convert UI “credits” at your edge if you sell packs that way. Recurring named credits attach to a bundle via `bundles include` and mint entitlement pools; one named currency per bundle, shareable across plans. See Credit systems. ## Outcome-based Sell a result, not a token count. Define an outcome with a spend ceiling for one run, pass the same `outcome` and `outcome_id` on every call that belongs to that job, and charge in your product when the run completes (or include N runs in a plan). Seams enforces the ceiling and attributes spend; your checkout still owns the invoice for “one reply.” ## Where margin sits | Place | When to use it | | --- | --- | | Bundle markup % | Fallback margin on provider cost when a model has no sell price | | Sell price per model | You publish fixed $/MTok input and output on the bundle | | At top-up | They buy $20 of balance; you keep a share once, calls run at cost-ish | | In the plan price | Subscription revenue covers allotment; overage is separate if you allow it | ## Ceilings are not prices A balance is what they have. A spend cap is how fast one key or plan may spend. An outcome ceiling is how much one job may cost. None of those is the product price you show on a pricing page, they are the enforcement behind it. - **[Usage-based pricing](https://docs.ourseams.com/example-usage-based-pricing)**: Markup, wallet, and a hard key cap, end to end. - **[Outcome-based pricing](https://docs.ourseams.com/example-outcome-based-pricing)**: Sell a job, budget the run, attribute every call. - **[Credit systems](https://docs.ourseams.com/credit-systems)**: Wallet top-ups vs plan allotments, and named currencies. - **[Outcomes](https://docs.ourseams.com/outcomes)**: Define ceilings and attach runs to gateway calls. --- # Subscriptions and overage > What a paid bundle mints each period, and what happens when it runs out. > Section: Core concepts · [https://docs.ourseams.com/subscriptions](https://docs.ourseams.com/subscriptions) A bundle with a price is a subscription. What the end user gets for that price is a grant: an amount minted into their balance, either every period or once. Two customers on the same bundle get the same grant on the same schedule. ## Attach a recurring grant to a bundle Allotments live on the bundle. Wallet top-ups (`seams users grant`) are separate, always micros, never a named-credit unit. A bundle can carry several includes in its one named currency; other bundles can share that currency or use their own. See Credit systems. #### CLI ```bash seams bundles include pro monthly-credits \ --name "Monthly credits" --unit credits --amount 30M # SLUG DISPLAY NAME UNIT AMOUNT THEN ON DEMAND RECURRING # monthly-credits Monthly credits credits 30000000 false true ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) await seams.bundles.include("pro", { slug: "monthly-credits", displayName: "Monthly credits", unit: "credits", amount: "30000000", price: null, thenOnDemand: false, recurring: true, }) ``` #### Python ```python import os from seams import Seams, models seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) seams.bundles.include( "pro", models.IncludeGrantRequest( slug="monthly-credits", display_name="Monthly credits", unit="credits", amount="30000000", price=None, then_on_demand=False, recurring=True, ), ) ``` | Flag | Means | | --- | --- | | `--unit credits` · `--unit micros` | Grant your own credits, or grant money | | `--amount` | How much, `30M` credits or an amount in dollars | | `--price` | What this grant sells for, when it is sold separately | | `--then-on-demand` | Allow overage once this grant is gone | | `--no-recurring` | Mint it once at sign-up rather than every period | ## The order money is spent in A call draws on the first grant that can cover it, then on-demand if you allowed it. Nothing falls through to on-demand by accident, because the last step has to be turned on. ```text monthly-tokens 30M tokens spent └─ topup-credit $4.20 left spent └─ then on-demand only if --then-on-demand ``` ## Turn overage on for one customer On-demand is per end user, with its own ceiling, so agreeing to overage with one customer is not a change to your bundle. #### CLI ```bash seams portal on-demand alice --enable --limit 25 seams portal spending alice ``` #### TypeScript ```ts await seams.subscription.onDemand({ endUserId: "alice", enabled: true, limitMicros: "25000000", }) await seams.subscription.show({ endUserId: "alice" }) ``` #### Python ```python seams.subscription.on_demand( end_user_id="alice", enabled=True, limit_micros="25000000", ) seams.subscription.show(end_user_id="alice") ``` ## Grant money instead of overage To simply add money rather than allow overage, grant it. A grant lands in the balance immediately and is spent before any overage would be. The CLI takes dollars; the SDKs take micro-dollars. #### CLI ```bash seams users grant 25.00 --user alice --reason top_up ``` #### TypeScript ```ts await seams.grants.issue({ endUsers: ["alice"], amountUnits: "25000000", reason: "top_up", }) ``` #### Python ```python seams.grants.issue( end_users=["alice"], amount_units="25000000", reason="top_up", ) ``` ## When it runs out The call is refused with a 402 before it reaches a provider, and the code says which ceiling stopped it. A stream already running is aborted mid-answer rather than finishing on money the customer does not have. | Code | Means | | --- | --- | | `insufficient_credit` | Available balance would not cover the reservation | | `spend_limit_exceeded` | A cap or bundle spend limit is reached for this period | - **[Credit systems](https://docs.ourseams.com/credit-systems)**: Design your own credits, packs, and plan allotments. - **[Worked example](https://docs.ourseams.com/example-credit-system)**: Welcome credit, packs, and a monthly Pro allotment. --- # Credit systems > Name your own credits, decide when they mint, and let the gateway spend them down. > Section: Core concepts · [https://docs.ourseams.com/credit-systems](https://docs.ourseams.com/credit-systems) Seams meters every call in money. Your credit system is the product vocabulary on top: what one credit means, who gets how many, and what happens when they run out. Everything below is scoped to one application, the active `sk_`, not shared across products. ## Two paths: wallet top-up vs plan allotment These are different nouns. Mixing them up is the usual way to mis-price a pack or attach the wrong unit. | | Wallet top-up | Plan allotment | | --- | --- | --- | | What it is | Money on the end user's wallet | An entitlement pool minted from a bundle include | | Unit on the wire | Always micros (dollars on the CLI) | `micros` or `credits`, set on the include | | How you mint | `seams users grant` / `grants.issue` | `seams bundles include …` | | Tied to a bundle? | No. Optional `--bundle` only picks who gets credited | Yes. The include lives on that plan | | When it refills | When you grant again (checkout, support, trial) | Each period, daily, or once, per the include | > `grants.issue` has no unit field and never credits a named-credit pool. If your UI sells “credits” as a wallet pack, convert to micros at a fixed rate before you call it. ## CLI: top up a wallet Amount is dollars. Recipients are named one way: specific end users, everyone on a bundle, or everyone in the application. #### CLI ```bash seams users grant 20.00 --user ada --reason top_up # --bundle here means "everyone on pro", not "credit in pro's unit" seams users grant 5.00 --bundle pro --reason apology seams users grant 2.00 --all --reason welcome ``` #### TypeScript ```ts await seams.grants.issue({ endUsers: ["ada"], amountUnits: "20000000", // $20.00 reason: "top_up", idempotencyKey: stripeEvent.id, }) ``` #### Python ```python seams.grants.issue( end_users=["ada"], amount_units="20000000", # $20.00 reason="top_up", idempotency_key=stripe_event.id, ) ``` ## CLI: attach an allotment to a plan The include is the template. Subscribing an end user to that bundle opens the pool; the gateway spends entitlement pools before the money wallet. #### CLI ```bash seams bundles include pro monthly-credits \ --name "Monthly credits" --unit credits --amount 500 # optional pack sold separately (once), still defined on the bundle seams bundles include pro pack-500 \ --name "500 credits" --unit credits --amount 500 \ --price 5.00 --no-recurring ``` #### TypeScript ```ts await seams.bundles.include("pro", { slug: "monthly-credits", displayName: "Monthly credits", unit: "credits", amount: "500", price: null, thenOnDemand: false, recurring: true, }) ``` #### Python ```python seams.bundles.include( "pro", models.IncludeGrantRequest( slug="monthly-credits", display_name="Monthly credits", unit="credits", amount="500", price=None, then_on_demand=False, recurring=True, ), ) ``` ## Named credit currencies A named credit currency is defined once for your organization, then attached to a bundle. One bundle sells in at most one named currency; several includes on that bundle (monthly allotment, bonus, purchasable pack) share it. Across bundles you choose: reuse the same currency everywhere, or give each plan its own. | Setup | What it means | | --- | --- | | Share one currency | `starter` and `pro` both point at the same unit, one “credit” means the same spend everywhere | | Different per plan | Each bundle has its own unit (or its own value override), “credit” on Pro can be worth more than on Free | | Several includes, one currency | One bundle can mint many allotments; they all burn in that bundle’s currency | | Money beside credits | The wallet is always micros and independent of the plan currency; top-ups never inherit the bundle’s unit | > The meter still prices usage in sell-micros. Named credits convert with the rate on the bundle (or the currency’s default). Customers should never see micro-dollars. ## What you design | Choice | What it decides | | --- | --- | | Unit | Dollars on the wallet, or a named currency on the plan | | Mint | At sign-up, on a plan period, after a pack purchase, or by support | | Spend order | Plan allotments first, then wallet top-ups, then overage if allowed | | Ceiling | A key or bundle cap so a loop cannot empty the account | | Empty state | A 402 your product turns into top-up, wait, or upgrade | ## UI credits that grant money When you sell “credits” but land them on the wallet, pick one rate and keep it in one module. The portal can show dollars; your product shows credits. ```ts // 1 credit = $0.01 of usage → micros = credits × 10_000 const MICROS_PER_CREDIT = 10_000n export function grantCredits(userId: string, credits: number, paymentId: string) { return seams.grants.issue({ endUsers: [userId], amountUnits: String(BigInt(credits) * MICROS_PER_CREDIT), reason: `pack:${credits}`, idempotencyKey: paymentId, }) } ``` ## When the balance is gone | Code | What your product should do | | --- | --- | | `insufficient_credit` | Offer a pack, upgrade, or wait for the next period | | `spend_limit_exceeded` | Stop the run; the period or key cap is the limit, not the wallet | > Pass your payment event id as `idempotencyKey` on every grant after checkout. Retries must not mint twice. - **[Pricing strategies](https://docs.ourseams.com/pricing-strategies)**: Usage-based, prepaid, plans, outcomes. - **[Worked example](https://docs.ourseams.com/example-credit-system)**: A writing product with welcome credit, packs, and a monthly plan. - **[Prepaid credits](https://docs.ourseams.com/example-prepaid-credits)**: Markup at top-up and the Stripe webhook shape. - **[Subscriptions and overage](https://docs.ourseams.com/subscriptions)**: Recurring grants and then-on-demand. - **[Wallets and holds](https://docs.ourseams.com/balances-and-holds)**: available = balance − reserved. --- # Spend caps > A hold before dispatch, and a stream that stops. > Section: Core concepts · [https://docs.ourseams.com/spend-caps](https://docs.ourseams.com/spend-caps) 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 #### CLI ```bash seams keys mint --user ada --bundle pro --cap 5.00 --label "Ada laptop" seams keys cap key_01J8Q…0x2m --limit 10 ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const key = await seams.keys.mint({ endUserId: "ada", bundle: "pro", capMicros: "5000000", name: "Ada laptop", }) await seams.keys.cap(key.id, { capMicros: "10000000" }) ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) key = seams.keys.mint( end_user_id="ada", bundle="pro", cap_micros="5000000", name="Ada laptop", ) seams.keys.cap(key.id, cap_micros="10000000") ``` ## 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 | Step | What happens | | --- | --- | | `reserve` | Hold the worst-case cost on the balance before dispatch | | `stream` | Re-check accumulated spend every 16 chunks and every 250 ms | | `abort` | Close the model provider connection and send the terminal frame | | `settle` | Release 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. #### TypeScript ```ts 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 ?? "") } ``` #### Python ```python for chunk in stream: reason = chunk.choices[0].finish_reason if reason == "spend_limit_exceeded": # the budget ran out. the answer is partial, the usage is final. top_up() break render(chunk.choices[0].delta.content or "") ``` > 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. #### CLI ```bash seams bundles edit pro --termination hard_token ``` #### TypeScript ```ts await seams.bundles.edit("pro", { terminationPolicy: "hard_token" }) ``` #### Python ```python seams.bundles.edit("pro", termination_policy="hard_token") ``` | Policy | What the end user sees | | --- | --- | | `hard_token` | The 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_message` | The 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. | | `grace` | The 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. --- # Outcomes > Budget a unit of work, not a request, when one job is several calls. > Section: Core concepts · [https://docs.ourseams.com/outcomes](https://docs.ourseams.com/outcomes) A spend cap on a key limits a period. An outcome limits a *job*: one support reply, one document summarised, one agent run. however many calls that takes. It is the right ceiling when the thing you sell is a result rather than a token. ## Define an outcome #### CLI ```bash seams outcomes add support-reply --name "Support reply" --limit 0.05 # OUTCOME DISPLAY NAME SPEND LIMIT USD RUNS SPENT USD # support-reply Support reply 0.05 4812 18.40 ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) await seams.outcomes.add("support-reply", { displayName: "Support reply", ceilingMicros: "50000", }) await seams.outcomes.show("support-reply") ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) seams.outcomes.add( "support-reply", display_name="Support reply", ceiling_micros="50000", ) seams.outcomes.show("support-reply") ``` ## Attach a run to a call Put the outcome on the request body. Same `outcome_id` across several calls is one run; a new id starts another. ```bash curl https://.gw.ourseams.com/v1/chat/completions \ -H "Authorization: Bearer ak_acme_…" \ -H "Content-Type: application/json" \ -d '{ "model": "acme/smart", "messages": [{ "role": "user", "content": "Draft the reply" }], "seams": { "outcome": "support-reply", "outcome_id": "ticket_8842" } }' ``` ## Read runs and archive #### CLI ```bash seams outcomes runs --outcome support-reply seams outcomes rm support-reply --yes ``` #### TypeScript ```ts await seams.outcomes.runs("support-reply") await seams.outcomes.rm("support-reply") ``` #### Python ```python seams.outcomes.runs("support-reply") seams.outcomes.rm("support-reply") ``` | Status | Means | | --- | --- | | `running` | Calls are still being attributed to this run | | `done` | The job finished inside its ceiling | | `capped` | The ceiling was reached; further calls in this run are refused | | `failed` | The job stopped for a reason that was not money | ## How it relates to the other ceilings Three limits can apply to one call, and the tightest wins. None of them replaces the others: a balance is what a customer has, a cap is how fast they may spend it, and an outcome is how much one job may cost. | Limit | Scope | Set on | | --- | --- | --- | | Available balance | Everything that end user does | The balance | | Spend cap | A period, per key | `keys.cap` | | Bundle spend limit | A period, per end user | `bundles.edit` | | Outcome ceiling | One run | `outcomes.add` | > `outcomes.rm` archives. Runs already recorded keep pointing at the outcome that produced them, so the history of what a job used to cost stays readable. - **[Outcome-based pricing](https://docs.ourseams.com/example-outcome-based-pricing)**: Sell a job, budget the run, charge in your product. - **[Pricing strategies](https://docs.ourseams.com/pricing-strategies)**: How outcomes sit next to usage and plans. - **[Spend caps](https://docs.ourseams.com/spend-caps)**: Period ceilings on keys and bundles. --- # Model aliases > Your names for models, not the provider's. > Section: Core concepts · [https://docs.ourseams.com/model-aliases](https://docs.ourseams.com/model-aliases) Your customers call `acme/smart`, not `anthropic/claude-sonnet-4`. The alias is the indirection that lets you change provider, change model, or run a fallback without your customers noticing or your prices moving. ```text acme/smart → anthropic/claude-sonnet-4 acme/fast → anthropic/claude-haiku-4.5 acme/vision → openai/gpt-5-vision ``` ## Create an alias #### CLI ```bash seams models add acme/smart --source anthropic/claude-sonnet-4 # ok acme/smart → anthropic/claude-sonnet-4 ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) await seams.models.add("acme/smart", { sources: [{ provider: "anthropic", model: "claude-sonnet-4" }], }) ``` #### Python ```python import os from seams import Seams, models seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) seams.models.add( "acme/smart", sources=[models.AddModelSource(provider="anthropic", model="claude-sonnet-4")], ) ``` ## Swap the source, pause, resume The same name with a different source is a swap; clients keep calling `acme/smart`. Disable stops serving without deleting the name; enable brings it back. #### CLI ```bash seams models add acme/smart --source openai/gpt-5 seams models disable acme/smart seams models enable acme/smart seams models show acme/smart seams models rm acme/smart --yes ``` #### TypeScript ```ts await seams.models.add("acme/smart", { sources: [{ provider: "openai", model: "gpt-5" }], }) await seams.models.disable("acme/smart") await seams.models.enable("acme/smart") await seams.models.show("acme/smart") await seams.models.rm("acme/smart") ``` #### Python ```python seams.models.add( "acme/smart", sources=[models.AddModelSource(provider="openai", model="gpt-5")], ) seams.models.disable("acme/smart") seams.models.enable("acme/smart") seams.models.show("acme/smart") seams.models.rm("acme/smart") ``` ## What an end user sees Aliases only. Usage rows and the portal show your name for the model; which provider served a call never crosses that boundary. The client keeps calling your name, any inbound wire: #### OpenAI ```ts await client.chat.completions.create({ model: "acme/smart", messages: [{ role: "user", content: "Summarise the ticket" }], }) ``` #### Anthropic ```ts await client.messages.create({ model: "acme/smart", max_tokens: 1024, messages: [{ role: "user", content: "Summarise the ticket" }], }) ``` #### Gemini ```ts await client.models.generateContent({ model: "acme/smart", contents: "Summarise the ticket", }) ``` #### curl ```bash curl https://.gw.ourseams.com/v1/chat/completions \ -H "Authorization: Bearer ak_acme_…" \ -H "Content-Type: application/json" \ -d '{ "model": "acme/smart", "messages": [{ "role": "user", "content": "Summarise the ticket" }] }' ``` > This is enforced at the response boundary, not by convention. A usage row that leaked a provider model id would be the bug this indirection exists to prevent. --- # Routers > A named policy over your model names, when one source is not enough. > Section: Core concepts · [https://docs.ourseams.com/routers](https://docs.ourseams.com/routers) A model name can point at several sources. A router is the policy that decides which one a call gets, kept as its own named object so the decision is reviewable and can change without touching a client. > Order of setup: create the target aliases, allow them on a bundle, then create the router. Creating a router fails if no bundle already allows every target. ## Create and inspect #### CLI ```bash seams routers add balanced --strategy round_robin \ --target acme/fast --target acme/smart # router balanced # strategy round_robin # targets acme/fast, acme/smart # attached to every bundle that allows both targets seams routers show balanced seams routers list ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) await seams.routers.add("balanced", { strategy: "round_robin", targets: [{ model: "acme/fast" }, { model: "acme/smart" }], }) await seams.routers.show("balanced") await seams.routers.list() ``` #### Python ```python import os from seams import Seams, models seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) seams.routers.add( "balanced", strategy="round_robin", targets=[ models.AddRouterTarget(model="acme/fast"), models.AddRouterTarget(model="acme/smart"), ], ) seams.routers.show("balanced") seams.routers.list() ``` ## How it attaches to a bundle You do not run `bundles allow` with the router slug. Allow the target aliases on the bundle first. When you create (or replace) the router, Seams attaches it to every bundle that already allows all of those targets. A key minted under that bundle may then call the router slug or any allowed alias. > If you later allow the same targets on another bundle, create or replace the router again so it attaches there too. ## Strategies | Strategy | Picks | | --- | --- | | `cheapest` | The lowest priced target your bundle can serve. The default | | `round_robin` | Walks targets in order, wrapping; cursor is process-local | | `random` | A uniform random target among those that can serve the call | > All three skip a target that cannot serve the call, no tool support when the call carries tools, no vision when it carries an image, a context window too short for the prompt. A router whose every target fails that check returns `no_capable_model` rather than picking one that would fail. > Order matters for `round_robin`. `--target` is repeatable and the order you pass is the walk order. ## What a caller sees They call the router by its slug, the same way they call any other model name. The response still says that name. Which target answered stays on the request record you see in the console and the request log. Targets can be different providers; the inbound client does not care. #### OpenAI ```ts const res = await client.chat.completions.create({ model: "balanced", messages: [{ role: "user", content: "Say hello" }], }) // caller still sees model: "balanced" ``` #### Anthropic ```ts const res = await client.messages.create({ model: "balanced", max_tokens: 1024, messages: [{ role: "user", content: "Say hello" }], }) ``` #### Gemini ```ts const res = await client.models.generateContent({ model: "balanced", contents: "Say hello", }) ``` #### curl ```bash curl https://.gw.ourseams.com/v1/chat/completions \ -H "Authorization: Bearer ak_acme_…" \ -H "Content-Type: application/json" \ -d '{ "model": "balanced", "messages": [{ "role": "user", "content": "Say hello" }] }' # route balanced · round_robin # route picked openai/gpt-5 (or anthropic/… or google/…) # 200 caller still sees model: "balanced" ``` ## Remove #### CLI ```bash seams routers rm balanced --yes ``` #### TypeScript ```ts await seams.routers.rm("balanced") ``` #### Python ```python seams.routers.rm("balanced") ``` ## One target per call A router picks one target and the call goes there. If that provider returns an error, the error reaches your caller. The router does not walk on to the next target, and it does not send the same call to several providers to compare answers. Handle a failed call the way you handle any other error from a model provider: read the code on the response and decide. > Cross-protocol examples (OpenAI client against an Anthropic target, and the reverse) are on Calling the gateway. --- # Provider accounts > Your provider accounts, what we do with them, and the catalog they draw from. > Section: Core concepts · [https://docs.ourseams.com/providers](https://docs.ourseams.com/providers) A provider account is one credential of yours at one model provider, under a label you choose. Models resolve to an account, so the account is where cost, health and rotation live. ## Connect #### CLI ```bash seams providers connect production --provider anthropic --key sk-ant-… # provider anthropic # label production # last four 8c21 # status active ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const account = await seams.providers.connect("production", { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY!, }) ``` #### Python ```python import os from seams import Seams, models seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) account = seams.providers.connect( "production", provider="anthropic", api_key=os.environ["ANTHROPIC_API_KEY"], ) ``` > The label is the path, so connecting twice under the same label replaces the credential rather than creating a second account. That is what makes the call safe to run from a deploy script. ## Health, rotation, and negotiated rates `probe` sends one cheap call and records the result. Rotating replaces the plaintext behind the same label. Override list prices when you have negotiated rates, margin arithmetic uses your number. #### CLI ```bash seams providers probe production seams providers rotate production --key sk-… seams providers price production gpt-4o-mini --input 0.10 --output 0.40 seams providers show production seams providers models production ``` #### TypeScript ```ts await seams.providers.probe("production") await seams.providers.rotate("production", { apiKey: process.env.OPENAI_API_KEY! }) await seams.providers.price("production", { model: "gpt-4o-mini", override: { input: "100000", output: "400000", cachedInput: null }, }) await seams.providers.show("production") await seams.providers.models("production") ``` #### Python ```python seams.providers.probe("production") seams.providers.rotate("production", api_key=os.environ["OPENAI_API_KEY"]) seams.providers.price( "production", model="gpt-4o-mini", override=models.ProviderPrice(input="100000", output="400000", cached_input=None), ) seams.providers.show("production") seams.providers.models("production") ``` > This is the cost side, not the sell side. What your end users pay is set on the bundle; the two are deliberately separate numbers so margin is a subtraction rather than a guess. ## The catalog The catalog is the curated list we know how to price and route. It is what `models.add --source` draws from. #### CLI ```bash seams catalog providers seams catalog models seams catalog show anthropic/claude-sonnet-4 ``` #### TypeScript ```ts await seams.catalog.providers() await seams.catalog.models() await seams.catalog.show("anthropic/claude-sonnet-4") ``` #### Python ```python seams.catalog.providers() seams.catalog.models() seams.catalog.show("anthropic/claude-sonnet-4") ``` | Command | Answers | | --- | --- | | `catalog.models` | Every curated model, with price and context window | | `catalog.show` | One model in full | | `providers.models` | What this account has actually loaded | > A model we cannot price is refused rather than estimated. `usage_unmeasurable` is a 500 that pages, on purpose: an incident is better than a guessed bill. --- # Bring your own key > Your provider account, sealed with KMS. > Section: Core concepts · [https://docs.ourseams.com/byok](https://docs.ourseams.com/byok) A provider credential is sealed on write under a per-customer encryption context. The wrapping key never leaves the KMS, and the plaintext exists only inside a callback that lasts one request. ## Connect a provider key #### CLI ```bash seams providers connect production --provider openai --key sk-… # provider openai # label production # last four 8c21 # status active ``` #### TypeScript ```ts import { Seams } from "@ourseams/sdk" const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! }) const account = await seams.providers.connect("production", { provider: "openai", apiKey: process.env.OPENAI_API_KEY!, }) // account.label · account.lastFour ``` #### Python ```python import os from seams import Seams seams = Seams(api_key=os.environ["SEAMS_API_KEY"]) account = seams.providers.connect( "production", provider="openai", api_key=os.environ["OPENAI_API_KEY"], ) # account.label · account.last_four ``` After connect, mint end-user keys as usual. Calls with those `ak_` keys use your provider account; you keep the provider bill, and Seams meters and caps the spend. ## Probe, rotate, disconnect A credential is replaced, never read back. Probe checks health; rotate swaps the plaintext behind the same label; `rm` disconnects. #### CLI ```bash seams providers probe production seams providers rotate production --key sk-… seams providers rm production --yes ``` #### TypeScript ```ts await seams.providers.probe("production") await seams.providers.rotate("production", { apiKey: process.env.OPENAI_API_KEY! }) await seams.providers.rm("production") ``` #### Python ```python seams.providers.probe("production") seams.providers.rotate("production", api_key=os.environ["OPENAI_API_KEY"]) seams.providers.rm("production") ``` > The resolved cache is in-process and nowhere else: any shared store persists to disk, replicates over a network, and shows up in whatever inspection tool it ships. --- # Emitters > Settled usage, delivered where you bill from. > Section: Core concepts · [https://docs.ourseams.com/emitters](https://docs.ourseams.com/emitters) An emitter is a destination for settled usage: an https endpoint you own, a Metronome ingest, or an Orb ingest. Every call that settles is delivered to each enabled emitter on the application, after the money has been recorded, so a destination that is down cannot change what a call cost or make the caller wait for it. ```bash seams emitters add webhook --url https://example.com/seams/usage # emitter emt_01K2VY9WQ4X00000000EMT # kind webhook # name https://example.com/seams/usage # enabled true # destination https://example.com/seams/usage # events usage.recorded # customer id end_user_id # credential …9f2c # signing secret whsec_… ``` > The signing secret is in that response and nowhere else. Store it before you run the next command. ## Managing them | Command | What it does | | --- | --- | | `seams emitters list` | Every destination, where it sends and whether it is on | | `seams emitters show ` | One destination, its events and the last four of its credential | | `seams emitters add webhook --url ` | Post batches to an endpoint you own | | `seams emitters add metronome --api-token --event-type ` | Ingest into Metronome | | `seams emitters add orb --api-key --event-name ` | Ingest into Orb | | `seams emitters update --disable` | Stop delivering, keep the configuration | | `seams emitters update --api-token ` | Rotate the stored credential | | `seams emitters rm ` | Stop delivering there, and forget the credential | | `seams emitters test ` | Deliver one sample now | > The same six operations are `GET`, `POST`, `PATCH` and `DELETE` on `/v1/emitters`, and the console has a screen for them. Add takes `--name` to label a destination and `--disabled` to configure one without turning it on. ## One event `usage.recorded` fires when a call settles and writes a usage row. It is the only event an emitter carries; any other name is refused when you configure it, rather than accepted and never delivered. ```bash seams emitters add webhook --url https://example.com/usage --events usage.settled # error # code unknown_event # message no emitter event 'usage.settled': usage.recorded # fix seams emitters add webhook --events usage.recorded ``` ## What a webhook receives A batch is posted as JSON with two headers: `seams-signature`, and `seams-idempotency-key` naming the batch. The body carries the settled amounts as decimal strings of micro-dollars, so no JSON number can round a balance. ```json { "id": "usgb_4b1f7c0a92e5f3d18a6c07b2e9d45183", "type": "usage.recorded", "events": [ { "idempotencyKey": "usg_01JB2X8Q7M4K00000000SET", "usageRecordId": "usg_01JB2X8Q7M4K00000000SET", "organizationId": "org_01JQZ8N4KP0000000000ACME", "endUserId": "eu_01JQZ8N4KP00000000000ANNA", "apiKeyId": "key_01JQZ8N4KP000000000000KEY", "requestId": "req_01JQZ8N4KP000000000000REQ", "provider": "openai", "model": "openai/gpt-4o-mini", "streamed": false, "tokens": { "inputTokens": 1204, "outputTokens": 318, "cachedInputTokens": 0, "reasoningTokens": 0, "tokenCountSource": "provider_reported" }, "providerCostMicros": "3044", "billedAmountMicros": "3652" } ] } ``` > There is no prompt and no completion here, and there is nowhere for one to appear: the usage record has no body column to read from. ## Verify before you parse The signature header carries the second it was signed and an HMAC-SHA256 over `t`, a full stop, and the raw body, keyed with your signing secret. Sign the same string yourself and compare in constant time. A body you have parsed is a body you have already trusted. ```http seams-signature: t=1756060800,v1=5257a869e7ecebeda32affa62cdca3fa… ``` ```ts import { createHmac, timingSafeEqual } from "node:crypto" const verify = (rawBody: string, header: string, secret: string): boolean => { const parts = new URLSearchParams(header.replace(/,/g, "&")) const t = parts.get("t") ?? "" const v1 = parts.get("v1") ?? "" const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex") return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) } ``` ## Delivery Delivery is at-least-once, so your handler has to be idempotent. Two keys make that cheap: the POST carries `seams-idempotency-key` for the whole batch, and every event inside carries an `idempotencyKey` equal to its usage record id. Store either one and a repeat is a no-op. A batch that is not accepted is retried twice more, after 200ms and then 400ms. `408`, `425`, `429`, `500`, `502`, `503` and `504` are treated as worth retrying; any other status is taken as an answer. The body is identical on every attempt, though each attempt is signed afresh, so `t` and `v1` differ. > There is no queue behind those three attempts. A batch that fails all three is not delivered later, and the call it describes is still in `seams usage`, `seams requests` and `GET /v1/usage`. so reconcile from there, and answer `2xx` on receipt rather than after your own downstream work. ## Where a webhook may point A webhook url must be `https`, and its host must resolve to a public address. Private ranges, loopback and link-local are refused, so a url cannot be used to reach inside our network. The address is checked on every delivery, not once when you save it, and a url that resolves somewhere private is a configuration fault rather than something to retry. ## Metronome Settled usage arrives as ingest events. `transaction_id` is the usage record id, which is what stops a repeat being rated twice, and `event_type` is the one you named when you added the emitter. ```bash seams emitters add metronome \ --api-token mtr_… \ --event-type app.inference \ --customer-id organization_id ``` ```json [ { "transaction_id": "usg_01JB2X8Q7M4K00000000SET", "customer_id": "org_01JQZ8N4KP0000000000ACME", "event_type": "app.inference", "timestamp": "2026-09-07T09:20:00.000Z", "properties": { "provider": "openai", "model": "openai/gpt-4o-mini", "streamed": false, "input_tokens": 1204, "output_tokens": 318, "cached_input_tokens": 0, "reasoning_tokens": 0, "provider_cost_micros": "3044", "billed_amount_micros": "3652", "request_id": "req_01JQZ8N4KP000000000000REQ", "api_key_id": "key_01JQZ8N4KP000000000000KEY" } } ] ``` ## Orb The same events, and the same properties, in Orb's shape. `idempotency_key` is again the usage record id, and `event_name` is the one you named. ```bash seams emitters add orb --api-key orb_… --event-name inference ``` ```json { "events": [ { "idempotency_key": "usg_01JB2X8Q7M4K00000000SET", "external_customer_id": "eu_01JQZ8N4KP00000000000ANNA", "event_name": "inference", "timestamp": "2026-09-07T09:20:00.000Z", "properties": { "provider": "openai", "billed_amount_micros": "3652" } } ] } ``` ## The properties both vendors receive | Property | What it holds | | --- | --- | | `provider` | Which vendor served the call | | `model` | The model that answered, not the name that was asked for | | `streamed` | Whether the call was streamed | | `input_tokens`, `output_tokens` | Counted for the attempt that settled | | `cached_input_tokens`, `reasoning_tokens` | Zero when the provider reports neither | | `provider_cost_micros` | What the call cost to serve, as a decimal string | | `billed_amount_micros` | What it billed, as a decimal string | | `request_id`, `api_key_id` | Present when the call carried them | ## Which customer the usage is filed under `--customer-id end_user_id` is the default and sends the end user the key belongs to, falling back to the organization when a call has no end user. `--customer-id organization_id` files everything under the customer instead, which is what you want when you bill your account rather than their customers. > The id on the wire is ours. `eu_…`, not the external id you minted the key with. `seams users show alice` prints both, so map them once when you create the customer in Metronome or Orb. ## Credentials Every credential is sealed on the row with an envelope key, and only the delivery path opens it. Nothing else can read one back: `list` and `show` print the last four characters and the destination, never the secret. A Metronome token or an Orb key is replaced in place with `seams emitters update --api-token …` or `--api-key …`. A webhook's signing secret is not: it is issued once when the emitter is added, so rotating it means removing the emitter and adding it again. ## Check it before real usage depends on it `test` delivers one sample `usage.recorded` now, with an id of its own, so a destination that cannot be reached says so here rather than at your first settled call. The sample belongs to no end user, so it is filed under your organization whichever customer id an emitter is set to. ```bash seams emitters test emt_01K2VY9WQ4X00000000EMT # emitter emt_01K2VY9WQ4X00000000EMT # kind webhook # destination https://example.com/seams/usage # event usage.recorded # idempotency key usg_01K2VY9WQ4X0000SAMPLE # # sample # usage record id usg_01K2VY9WQ4X0000SAMPLE # customer id org_01JQZ8N4KP0000000000ACME # request id req_01K2VY9WQ4X0000SAMPLE # provider sample # model sample # streamed false # input tokens 1820 # output tokens 310 # billed amount micros 4200 # # delivered true # reason # latency ms 142 # sent at 2026-09-07T09:20:00.000Z ``` --- # Agent onboarding > An agent that provisions its own key, its own budget and its own base URL, no console visit. > Section: Integrations · [https://docs.ourseams.com/agent-onboarding](https://docs.ourseams.com/agent-onboarding) When the thing integrating is an agent rather than a person, the console is in the way. Two operations are all your backend needs: mint a key under a bundle you allow (that provisions the agent as an end user), and put a ceiling on that key. or pass the ceiling on mint. ## The flow ```text agent your backend seams ───── ──────────── ───── "provision me" → validate the request POST /v1/keys → upsert end user + mint ak_ (+ cap) ← { id, key, capMicros } ← { baseUrl, apiKey } call the base URL ──────────────────────────────→ entitled, routed, capped ``` > The agent never sees your `sk_`. Your backend holds it and mints on the agent's behalf, which is what keeps one runaway agent from being able to mint itself a bigger budget. ## Provisioning endpoint ### 1. Take the request and decide what it is allowed The agent identifies itself however your product already does it. You choose the bundle and the ceiling; the agent does not. #### typescript ```ts // app/api/agents/provision/route.ts export async function POST(req: Request) { const { agentName, ownerId } = await verifiedBody(req) const key = await seams.keys.mint({ endUserId: `${ownerId}:${agentName}`, name: agentName, bundle: "agent-tier", capMicros: "2000000", }) return Response.json({ baseUrl: "https://.gw.ourseams.com/v1", apiKey: key.key, capMicros: key.capMicros, }) } ``` #### python ```python # routes/agents.py @router.post("/agents/provision") async def provision(request: Request): body = await verified_body(request) agent_id = f"{body.owner_id}:{body.agent_name}" key = seams.keys.mint( end_user_id=agent_id, name=body.agent_name, bundle="agent-tier", cap_micros="2000000", # $2.00 ) return { "baseUrl": "https://.gw.ourseams.com/v1", "apiKey": key.key, "capMicros": key.cap_micros, } ``` ### 2. The agent stores what it got and calls Two values are all it needs. OpenAI, Anthropic, and Gemini clients all work from here, same host, same `ak_`. ```python import os, httpx from openai import OpenAI from anthropic import Anthropic from google import genai from google.genai import types as google_types creds = httpx.post( "https://acme.example.com/api/agents/provision", json={"agentName": "nightly-summariser", "ownerId": "org_42"}, ).json() openai = OpenAI(base_url=creds["baseUrl"] + "/v1", api_key=creds["apiKey"]) anthropic = Anthropic(base_url=creds["baseUrl"], api_key=creds["apiKey"]) gemini = genai.Client( api_key=creds["apiKey"], http_options=google_types.HttpOptions(base_url=creds["baseUrl"]), ) ``` ### 3. Handle the ceiling A capped agent gets a 402 with `spend_limit_exceeded`. That is backpressure, not a failure: stop the run and let the next window pick it up. ```python try: res = openai.chat.completions.create(model="acme/fast", messages=msgs) except Exception as err: if getattr(err, "status_code", None) == 402: mark_run_capped() # the window will roll else: raise ``` ## Rotation There is no rotate: you mint a new key and revoke the old one, in that order, so the agent is never without one. Because attribution is per end user, the agent's history survives the swap. #### cli ```bash seams keys mint --user org_42:nightly-summariser --bundle agent-tier seams keys revoke key_01J.. --yes ``` #### typescript ```ts const fresh = await seams.keys.mint({ endUserId: agentId, bundle: "agent-tier" }) await seams.keys.revoke("key_01J...") // hand fresh.key to the agent; the old one stops working immediately ``` #### python ```python fresh = seams.keys.mint(end_user_id=agent_id, bundle="agent-tier") seams.keys.revoke("key_01J...") # hand fresh.key to the agent; the old one stops working immediately ``` --- # CLI > Mint keys, read what spend went where, and check a broken integration without opening the console. > Section: Integrations · [https://docs.ourseams.com/cli](https://docs.ourseams.com/cli) ```bash npm install -g @ourseams/cli seams auth login --token sk_acme_… ``` ## The four you will actually use ### 1. Mint a key Name the end user with your own id; mint creates them if they are new. The secret prints once. Use `seams users create` only when you want a balance without a key yet. ```bash seams keys mint --user alice --bundle pro --cap 50 --label "alice's laptop" # key ak_acme_… # key prefix ak_acme_simula # last four 9f2c # end user id alice # warning shown once; store it now ``` ### 2. Find out why a call failed `doctor` walks the five misconfigurations behind most support requests and exits non-zero on an error, so it works as a CI gate. ```bash seams doctor --table # status error # # LEVEL CODE DETAIL FIX # error model_no_live_source cheap has no enabled source connect a provider account # warn supply_unhealthy anthropic has failed its last 4 probes seams providers rotate anthropic --key # warn end_user_frozen dara is frozen and cannot call seams users unfreeze dara ``` ### 3. Grant credit Takes dollars and converts; the ledger stores micro-dollars. One grant reaches one end user or fifty, and every balance moves or none does. ```bash seams users grant 25.00 --user alice --user bilal --reason top_up # recipients 2 # amount each usd 25.00 # total usd 50.00 # reason top_up # end users alice, bilal ``` ## Everything else | Command | What it does | | --- | --- | | `seams keys list` | Every key, masked to its prefix, with its cap and what it has spent | | `seams keys revoke ` | Revoked immediately; mint a replacement to rotate | | `seams usage --since 30d` | Calls, tokens, provider cost and what it was charged at, by model name | | `seams margin --since 30d` | What supply cost, what end users were charged, and what was kept | | `seams bundles list` | Bundles, their price, markup, spend limit and model count | | `seams providers probe