Your first agent generation
By the end of this tutorial you will have run an agent and know, to the token, what the run cost.
Three steps:
- Create an agent bound to your provider.
- Run a generation.
- Check what it cost.
Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
-
A credential. A
nat_sk_…API key (or a session JWT from Auth) exported asNATURALI_TOKEN, and your client set up — the CLI, the SDK or plaincurlagainsthttps://api.naturali.ai/v1:export NATURALI_TOKEN=nat_sk_...export NATURALI_API=https://api.naturali.ai/v1 # curl examples only -
A project with a working provider in it. That is exactly what Create a provider builds — do it first if you haven't. Arrive here with both ids exported:
export PROJECT=proj_V1StGXR8Z5jdHi6Bexport PROVIDER=aip_V1StGXR8Z5jdHi6Bexport NATURALI_PROJECT=$PROJECT # lets the CLI omit --project-id
Whether your provider is managed (naturali models) or BYOK (your own key) changes nothing until step 3, where it decides whether the receipt carries a dollar figure — a managed run is priced by naturali, a BYOK run reports tokens only.
1. Create an agent
An agent binds a provider to instructions. Only
provider_id is required — model, temperature and step limits all fall back to
the provider and platform defaults.
- CLI
- SDK
- curl
naturali create-agent \
--provider-id aip_V1StGXR8Z5jdHi6B \
--name geography-tutor \
--instructions 'You are a concise geography tutor. Answer in one sentence.'
const { data: agent } = await naturali.agents.createAgent({
path: { project_id: PROJECT },
body: {
provider_id: PROVIDER,
name: 'geography-tutor',
instructions: 'You are a concise geography tutor. Answer in one sentence.',
},
});
curl -sS -X POST "$NATURALI_API/projects/$PROJECT/agents" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"provider_id": "aip_V1StGXR8Z5jdHi6B",
"name": "geography-tutor",
"instructions": "You are a concise geography tutor. Answer in one sentence."
}'
{
"id": "agent_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"provider_id": "aip_V1StGXR8Z5jdHi6B",
"name": "geography-tutor",
"model": null,
"instructions": "You are a concise geography tutor. Answer in one sentence.",
"tool_bindings": [],
"status": "active"
}
model is null because you didn't set one — the agent runs on the provider's
default_model. Set model on the agent only when you want it to differ from
the provider default.
export AGENT=agent_V1StGXR8Z5jdHi6B
To give the agent tools, pass tool_bindings. This tutorial
stays on a plain model turn.
2. Run a generation
A generation is one model loop: send messages, the agent resolves its tools and runs, you get the output back.
Pass action_id while you're here. It is a label recorded on the usage
meter, so spend rolls up per operating action later — free to set now, impossible
to backfill.
Pass wait too. Generations run in the background by default: without it the
call answers 202 with a generation_id to poll, which is what you want in a
real application but not while you are reading output in a terminal.
- CLI
- SDK
- curl
naturali create-generation \
--agent-id agent_V1StGXR8Z5jdHi6B \
--wait \
--action-id tutorial.first-run \
--messages '[{"role":"user","content":"What is the capital of France?"}]'
const { data: generation } = await naturali.generations.createGeneration({
path: { project_id: PROJECT, agent_id: AGENT },
query: { wait: true },
body: {
action_id: 'tutorial.first-run',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
},
});
console.log(generation?.text); // 'Paris is the capital of France.'
curl -sS -X POST \
"$NATURALI_API/projects/$PROJECT/agents/$AGENT/generations?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"action_id": "tutorial.first-run",
"messages": [
{ "role": "user", "content": "What is the capital of France?" }
]
}'
{
"id": "gen_V1StGXR8Z5jdHi6B",
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"status": "completed",
"text": "Paris is the capital of France.",
"object": null,
"tool_calls": null
}
That id is what step 3 prices:
export GENERATION=gen_V1StGXR8Z5jdHi6B
Three things worth knowing before you move on:
- Background is the default. Drop
waitand you get202with{"status":"accepted","generation_id":"gen_…"}instead; read the outcome fromGET /v1/projects/{project_id}/generations/{generation_id}when it settles. That is the mode to reach for once a person is no longer watching the call — see Generations → Background by default. - Streaming. Add
"stream": trueand the response is a Server-Sent Events stream instead of one JSON body. A stream always waits. The cost is metered either way. requires_action. If the agent has client-side tools,statuscomes backrequires_actionwith pendingtool_callsinstead oftext.
3. Check what it cost
Ask the generation what it cost. The response is the billing-grade receipt — per-model line items, the quantities behind each charge, and the total.
- CLI
- SDK
- curl
naturali get-generation-usage --generation-id gen_V1StGXR8Z5jdHi6B
const { data: usage } = await naturali.generations.getGenerationUsage({
path: { project_id: PROJECT, generation_id: GENERATION },
});
console.log(usage?.cost_usd, usage?.total_tokens);
curl -sS \
"$NATURALI_API/projects/$PROJECT/generations/$GENERATION/usage" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"generation_id": "gen_V1StGXR8Z5jdHi6B",
"currency": "USD",
"cost_usd": 0.00000306,
"input_tokens": 19,
"output_tokens": 8,
"cached_tokens": 0,
"reasoning_tokens": 0,
"total_tokens": 27,
"by_meter_type": [{ "meter_type": "llm_tokens", "cost_usd": 0.00000306 }],
"line_items": [
{
"event_id": "uev_V1StGXR8Z5jdHi6B",
"meter_type": "llm_tokens",
"provider": "bedrock",
"model": "amazon.nova-lite-v1:0",
"cost_usd": 0.00000306,
"components": [
{
"component": "input_tokens",
"quantity": 19,
"unit": "token",
"billable": true,
"unit_price": 0.00000006,
"cost_usd": 0.00000114
},
{
"component": "output_tokens",
"quantity": 8,
"unit": "token",
"billable": true,
"unit_price": 0.00000024,
"cost_usd": 0.00000192
}
]
}
]
}
Those are real figures from the run above, not round numbers: one short
question on Nova Lite costs about three millionths of a dollar. A generation may
also carry an unpriced api_request line item alongside llm_tokens — it
appears in by_meter_type with a null cost and does not change the total.
If you came here straight from
Create a provider, expect cost_usd: null and
unit_price: null here even though you used a managed provider.
Registering a managed provider sets its price a few seconds into the future, so there is a short window right after it is created in which the provider exists and can generate but is not yet priced. Cost is frozen at write time and is not backfilled, so a generation that lands inside that window stays unpriced permanently — re-reading its receipt later will not fill the cost in.
Give the provider a moment after creating it, then run step 2 again. The new generation is priced, and its receipt is the one to read:
# too fast — this generation is permanently unpriced
provider created → generation 2s later → cost_usd: null
# a moment later — priced
provider created → generation 40s later → cost_usd: 0.00000102
This only affects the first few seconds of a provider's life. Every later generation on the same provider is priced normally, and BYOK is unaffected (it is never priced here at all).
How to read it:
cost_usdis the whole generation, frozen at write time — the price that applied when it ran, not today's price.input_tokensis the full prompt, withcached_tokensthe part of it that was served from cache (already counted insideinput_tokens, never added on top).total_tokensisinput_tokens + output_tokens.reasoning_tokensis reported for visibility and never priced — it is a subset ofoutput_tokens, not an extra charge.componentsis the arithmetic:quantity × unit_priceper billable dimension. This is where a surprising total gets explained.cost_usd: nullmeans nothing was priced — never that it was free. On BYOK that is the expected result: the run happened on your provider account, so your provider bills you and naturali reports the tokens without a price. On a managed provider,nullmeans the model had no price on file when the run happened.
Rolling it up
One generation is the receipt. For the aggregate, the
project meter buckets the whole project by model,
agent, run, day or meter_type over an optional window.
It is an aggregate, not a substitute for the receipt: a plain generation belongs
to no orchestration run, so group_by=run buckets it under a null key. Reach
for the per-generation receipt whenever you want a single turn priced.
- CLI
- SDK
- curl
naturali get-project-usage --group-by model --from 2026-07-01T00:00:00Z
const { data: usage } = await naturali.projects.getProjectUsage({
path: { project_id: PROJECT },
query: { group_by: 'model', from: '2026-07-01T00:00:00Z' },
});
curl -sS -G "$NATURALI_API/projects/$PROJECT/usage" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-d group_by=model -d from=2026-07-01T00:00:00Z
{
"project_id": "proj_V1StGXR8Z5jdHi6B",
"window": { "from": "2026-07-01T00:00:00.000Z", "to": null },
"group_by": "model",
"cost_usd": 0.00000306,
"input_tokens": 19,
"output_tokens": 8,
"cached_tokens": 0,
"total_tokens": 27,
"groups": [
{
"key": "amazon.nova-lite-v1:0",
"cost_usd": 0.00000306,
"input_tokens": 19,
"output_tokens": 8,
"cached_tokens": 0,
"total_tokens": 27
},
{
"key": "request",
"cost_usd": null,
"input_tokens": 0,
"output_tokens": 0,
"cached_tokens": 0,
"total_tokens": 0
}
]
}
The request group is the unpriced per-request meter; it carries no tokens and
no cost, and appears alongside the model buckets.
Use the per-generation receipt to price one turn, and the project meter to see where the money went.