Meter un-instrumented calls
Enforce and attribute LLM spend the Solwyn SDK does not see — a curl, a shell script, another language, or a local model
The Solwyn SDK wraps a Python client and handles enforcement and attribution automatically. When a call does not go through that wrapper, the CLI gives you the same two guarantees in three commands: reserve budget before the call, settle the reservation with what the call actually used, and, for calls that already happened, record them after the fact.
This page is the walkthrough. Agent integration covers the same loop for coding agents that call it as MCP tools.
The one rule
Never run this loop around a call the SDK already wraps. The SDK reserves and settles on its own; wrapping it again holds the budget twice and can make a project deny itself. Use the CLI only for spend the SDK does not see.
What you need
- A project key with full scope in
SOLWYN_API_KEY. Reserving and settling are writes, so a read-only key exits3at the first check. See Key scopes. - The project ID, as
--projectorSOLWYN_PROJECT_ID. jq, or any JSON parser, to read the reservation ID out of the check.
Reserve, spend, settle
Set a run ID once per job so every call in it is grouped, then run the three steps around each provider call.
export SOLWYN_AGENT_RUN_ID="nightly-$(date +%Y%m%d)"
export SOLWYN_TAGS="env=prod,team=research" # optional attribution tags
CALL_ID=$(uuidgen | tr A-F a-f) # one lowercase UUID per logical call
# 1. Reserve. A denied check exits 4, so the script stops here.
CHECK=$(solwyn --json budget check \
--provider openai --model gpt-4o --estimated-input-tokens 1200) || exit $?
RID=$(printf '%s' "$CHECK" | jq -r '.reservation_id // empty')
# 2. Spend. Make the provider call yourself, with your own provider key.
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"..."}]}')
IN=$(printf '%s' "$RESPONSE" | jq '.usage.prompt_tokens')
OUT=$(printf '%s' "$RESPONSE" | jq '.usage.completion_tokens')
# 3. Settle with the real token counts. Skip when the check returned no reservation.
if [ -n "$RID" ]; then
solwyn --json budget confirm \
--reservation-id "$RID" --call-id "$CALL_ID" \
--provider openai --model gpt-4o \
--input-tokens "$IN" --output-tokens "$OUT"
fiThree details matter:
- A denied check is exit
4, not an error. Treat it as "do not spend". In JSON the payload saysallowed: falseand names the budget that denied it. - A null reservation means fail-open. If the check returns
allowed: truewithreservation_id: null, Solwyn Cloud chose not to hold budget. There is nothing to settle, so skipconfirm. confirmrecords attribution for you. It uses the same call ID, so do not alsorecordthat call.
Calls that already happened
A call that ran with no reservation, such as a local model at zero cost, an offline replay, or a script written before the guardrail existed, is reported afterwards with record. It never touches the reservation plane; Solwyn Cloud prices it from the counts you report.
solwyn record --provider ollama --model llama3 --input-tokens 120 --output-tokens 30
solwyn --json record --batch events.json # a JSON array of events, chunked for youGenerate and persist a canonical lowercase call_id before sending. Core deduplicates (project_id, call_id, attempt_index) while retained; a duplicate is an acknowledged no-op, not a correction. Reuse identities when retrying attribution. A generated ID per invocation cannot protect a wholly lost reply, and a legacy_key collision does not prove call-ID durability. See record outcomes.
Beyond text tokens
Image, video, audio, and speech calls are priced by unit rather than by token. Pass the billed quantities to budget check so the call is priced and deniable before it runs, and to budget confirm so it settles at its real cost:
| Call type | Flags on check and confirm |
|---|---|
| Image generation | --image-count, --resolution, --quality |
| Video or audio | --video-seconds, --audio-seconds, --generation-count |
| Speech synthesis | --input-characters (a count, never the text) |
| Image-token models | --image-input-tokens, --image-output-tokens on confirm |
The full flag list, batch mode, and fallback declarations are on the budget reference.
Many calls at once
The standalone CLI budget check --batch validates a JSON array and submits legacy chunks of at most 32, preserving item order. Each allowed item has its own reservation, and any denial exits 4; this is not an atomic all-or-nothing batch. Failed chunks can have unknown effects, and there is no CLI journal recovery ID. MCP budget_check_batch instead uses a durable journal with a caller-retained UUIDv7 and GET-only recovery. See bounded MCP work.
Timing and recovery
Budget and quota periods use server receive time, while analytics use the stored event timestamp. A backdated recording counts when received and can appear on its original analytics day; backdating cannot bypass today's limits.
Settlement and recording are separate operations. Keep the original provider observations, stable call/attempt identities and the first exact returned recording_retry. After known settlement, retry only attribution; do not reserve again for completed work. For unit-priced calls, confirm observed media and actual served provider/model, keeping null as unobservable and zero as observed zero. Media settlement recovery covers lost replies and receipt expiry; Scripting distinguishes recognized unaccepted requests from unknown outcomes.
See also
- Agent integration — the same loop for coding agents, with the MCP tool names.
- Scripting and CI — the JSON envelope, exit codes, and retry rules.
budgetandrecord— every option.