Skip to main content

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:

  1. Create an agent bound to your provider.
  2. Run a generation.
  3. 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

  1. A credential. A nat_sk_… API key (or a session JWT from Auth) exported as NATURALI_TOKEN, and your client set up — the CLI, the SDK or plain curl against https://api.naturali.ai/v1:

    export NATURALI_TOKEN=nat_sk_...
    export NATURALI_API=https://api.naturali.ai/v1 # curl examples only
  2. 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_V1StGXR8Z5jdHi6B
    export PROVIDER=aip_V1StGXR8Z5jdHi6B
    export 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.

naturali create-agent \
--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.

naturali create-generation \
--agent-id agent_V1StGXR8Z5jdHi6B \
--wait \
--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 wait and you get 202 with {"status":"accepted","generation_id":"gen_…"} instead; read the outcome from GET /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": true and 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, status comes back requires_action with pending tool_calls instead of text.

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.

naturali get-generation-usage --generation-id gen_V1StGXR8Z5jdHi6B
{
"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.

A brand-new managed provider is briefly unpriced

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_usd is the whole generation, frozen at write time — the price that applied when it ran, not today's price.
  • input_tokens is the full prompt, with cached_tokens the part of it that was served from cache (already counted inside input_tokens, never added on top). total_tokens is input_tokens + output_tokens.
  • reasoning_tokens is reported for visibility and never priced — it is a subset of output_tokens, not an extra charge.
  • components is the arithmetic: quantity × unit_price per billable dimension. This is where a surprising total gets explained.
  • cost_usd: null means 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, null means 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.

naturali get-project-usage --group-by model --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.

What's next

  • Give the agent tools so it can do more than answer from the model alone.
  • Hold a conversation instead of one turn with sessions, which keep the transcript for you.
  • Look at what a run actually did, step by step, with traces.
  • Ground the agent in your own documents via knowledge.