# Sarcini și cheltuieli (/ro/docs/agents/tasks-and-spend)

Ce este o sarcină, cum apar plafoanele de plan și de cheltuieli ca 429, de ce o sarcină poate aștepta confirmarea ta și endpoint-urile /v1/tasks.

codai facturează per **sarcină**, nu per cerere. O sarcină este o unitate de muncă — o instrucțiune nouă de la un utilizator și tot ce face agentul ca să o termine. Turele de continuare (rezultate de unelte, pași de follow-up) aparțin sarcinii care le-a pornit și nu facturează nimic în plus.

## Când începe o sarcină [#când-începe-o-sarcină]

O cerere pornește o sarcină când este o tură de nivel superior al cărei **ultim mesaj este un mesaj real de utilizator** — text tastat de o persoană, nu un rezultat de unealtă și nu o continuare a asistentului. Follow-up-urile rapide într-o **fereastră de 10 minute** după o sarcină plătită sunt gratuite: contează ca aceeași sarcină. Un follow-up după fereastră deschide una nouă.

Sub-agenții porniți și pașii run-urilor de agent sunt întotdeauna continuări (`is_task_start = 0`). Un [run de agent sincron](./index.mdx) este exact o sarcină.

### `X-Codai-No-Task` [#x-codai-no-task]

Apelurile utilitare — un titlu, o clasificare, o reformulare pe un rând — nu ar trebui să consume o sarcină. Trimiți `X-Codai-No-Task: 1` pe `POST /v1/chat/completions` sau `POST /v1/messages` **cu `codai-fast` sau `codai-explorer`** și cererea se facturează ca o continuare. Pe orice alt alias, header-ul este `400 bad_request`:

> `x-codai-no-task is only valid for utility aliases (codai-fast, codai-explorer); got "codai"`

## Plafoane și cele două 429 [#plafoane-și-cele-două-429]

Înainte să ruleze o sarcină nouă, gateway-ul îți parcurge drepturile în această ordine și se oprește la primul care plătește:

1. Plan nelimitat → rulează.
2. Sold în portofel în EUR (credit de bun venit, referral-uri, alimentări) → rulează, taxat după fapt.
3. Sarcini incluse în planul tău luna aceasta (sau azi, pe planurile zilnice) → rulează.
4. Credite de sarcini preplătite → rulează.
5. Depășire contorizată (metered overage), dacă facturarea ta o permite → rulează.
6. Altfel **`429 quota_exceeded`**.

```json
{
  "error": {
    "message": "You've used all 5 agent tasks included in the Free plan this month. Your free tasks reset in 7d 3h. Upgrade to Starter for 33 verified tasks every month on the same codai model: https://codai.ro/pricing",
    "type": "rate_limit_error",
    "code": "quota_exceeded"
  }
}
```

`Retry-After` pe acel răspuns este onest: numărul de secunde până la resetarea plafonului (următoarea lună UTC, sau miezul nopții UTC la un plafon zilnic). Continuările nu sunt blocate niciodată — o sarcină care a început deja se termină întotdeauna.

**Celălalt** 429, `rate_limit_exceeded`, este despre rata de cereri și bugetele în euro, nu despre sarcini: limitele de cereri per minut și per oră (`X-RateLimit-Limit-Minute`, `-Remaining-Minute`, `-Reset-Minute` și trio-ul `-Hour` pe fiecare răspuns) și plafonul zilnic / lunar de cheltuieli în EUR setat pe o cheie în hub. Citești `Retry-After` și faci back off. Vezi [limite și prețuri](../gateway/limits-pricing.mdx) pentru tier-uri.

<Callout type="info">
  Ambele coduri poartă `type: "rate_limit_error"`, deci un SDK OpenAI ridică `RateLimitError` pentru
  amândouă. Le distingi prin `error.code`: `quota_exceeded` înseamnă cumperi sau aștepți resetarea;
  `rate_limit_exceeded` înseamnă încetinești.
</Callout>

## Facturare pe rezultat și confirmare [#facturare-pe-rezultat-și-confirmare]

Dincolo de plafon, o sarcină poartă un **rezultat (outcome)** — a rezolvat codai problema efectiv? Pe suprafețele care pot dovedi asta (desktop, CLI), clientul raportează `pass` sau `fail` cu dovezi de execuție și sarcina se decontează singură. Tot restul — IDE-uri, SDK-uri brute, curl, orice clasifică gateway-ul ca `proxy` — nu poate dovedi un rezultat, așa că gateway-ul te întreabă **pe tine**.

```text
open ──(inactiv 30 min)──▶ unconfirmed ──(răspunzi în 24 h)──▶ confirmed | fail
                                       └──(fără răspuns)────────▶ fereastră închisă, nefacturat
```

- O sarcină `proxy`, `cli` sau `mobile` fără activitate timp de **30 de minute** este închisă ca `unconfirmed` și primește o fereastră de confirmare de **24 de ore**.
- Apare sub **În așteptare** la [`hub.codai.ro/tasks`](https://hub.codai.ro/tasks) — _„Când o sarcină din IDE sau CLI devine inactivă, apare aici ca să ne spui dacă codai a rezolvat-o.”_ Răspunzi **Da, a funcționat** sau **Nu**, opțional cu o notă (păstrată cu sarcina, niciodată trimisă modelului).
- **Da** închide sarcina ca `confirmed` cu dovada `user_confirmed` și aplică taxa de succes (1,20 €; consumul de peste 1,20 € al sarcinii se facturează pentru diferență la tarifele standard pe tokeni); pe planurile cu portofel, costul de tokeni deja taxat este rambursat în contul ei. **Nu** o închide ca `fail` — nu se taxează nimic.

Hub-ul arată și `surface`-ul fiecărei sarcini (IDE / API, Desktop, CLI, Resolve, Sandbox, Mobile, Hub), numărul de cereri, costul upstream și ce s-a taxat, ca să poți reconcilia o factură rând cu rând.

Pentru a atribui sarcinile precis, trimiți `X-Codai-Client: <surface>/<version>` (vezi [header-ele cererii](../gateway/headers.mdx#identitatea-clientului)) și, dacă ai propriile id-uri de sarcină, `X-Codai-Task-Id` (`[A-Za-z0-9._:-]`, ≤ 128 caractere) — gateway-ul grupează cererile după el în loc de sesiunea derivată și îl returnează ca ecou.

## Obiectul sarcinii [#obiectul-sarcinii]

Fiecare răspuns `/v1/tasks` folosește această formă. Banii sunt în micro-unități întregi; datele sunt ISO-8601 sau `null`.

<TypeTable
  type={{
    id: { type: 'string', description: 'UUID.' },
    surface: {
      type: "'proxy' | 'desktop' | 'cli' | 'resolve' | 'sandbox' | 'mobile' | 'hub'",
      description: 'De unde a venit sarcina.',
    },
    session_key: {
      type: 'string | null',
      description: 'Sesiunea derivată, sau X-Codai-Task-Id-ul tău.',
    },
    [`client_task_id`]: {
      type: 'string | null',
      description: 'Id-ul tău de corelare, când ai trimis unul.',
    },
    title: {
      type: 'string | null',
      description: 'Primul mesaj de utilizator, fără spații la capete.',
    },
    outcome: {
      type: "'open' | 'pass' | 'fail' | 'error' | 'unconfirmed' | 'confirmed' | 'cancelled'",
      description: 'Starea curentă.',
    },
    evidence_kind: {
      type: "'exec_verdict' | 'reexec' | 'user_confirmed' | 'none'",
      description: 'Cum a fost stabilit rezultatul.',
    },
    evidence_ref: {
      type: 'string | null',
      description: 'Id-ul cererii sau al verdictului care susține dovada.',
    },
    requests: { type: 'integer', description: 'Cereri de model atribuite sarcinii.' },
    cost_micro_usd: { type: 'integer', description: 'Costul upstream de tokeni, micro-USD.' },
    charged_micro_eur: { type: 'integer', description: 'Cât ai fost taxat, micro-EUR.' },
    billed_success: { type: 'boolean', description: 'Dacă s-a aplicat taxa fixă de succes.' },
    billable_at: { type: 'string | null' },
    confirm_deadline_at: {
      type: 'string | null',
      description: 'Sfârșitul ferestrei de confirmare de 24 de ore pentru sarcinile neconfirmate.',
    },
    opened_at: { type: 'string' },
    last_activity_at: { type: 'string' },
    closed_at: { type: 'string | null' },
    note: { type: 'string | null', description: 'Nota ta de la o confirmare.' },
  }}
/>

## Endpoint-uri [#endpoint-uri]

Toate rutele `/v1/tasks` au nevoie de o cheie legată de un cont de utilizator (altfel `401 invalid_api_key` — `Tasks require a user-scoped API key`) și returnează doar sarcinile tale. Sunt rate-limitate ca orice altă cerere.

| Metodă | Cale                                | Returnează                                                                                                                                                                                             |
| ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET`  | `/v1/tasks?outcome=&limit=&cursor=` | `{ tasks: Task[], next_cursor: string \| null }`, cele mai noi primele. `outcome` filtrează după stare; `limit` 1 – 200 (implicit 50); `cursor` este `opened_at`-ul ultimului rând pe care l-ai văzut. |
| `GET`  | `/v1/tasks/pending`                 | `{ tasks: Task[] }` — până la 100 de sarcini care așteaptă răspunsul tău (aceeași listă ca tab-ul **În așteptare** din hub).                                                                           |
| `GET`  | `/v1/tasks/stats?since=`            | Numărători `{ since, opened, confirmed, pass, unconfirmed, billed }` de la un timestamp ISO (implicit: acum 30 de zile).                                                                               |
| `GET`  | `/v1/tasks/:id`                     | O sarcină, sau `404 not_found`.                                                                                                                                                                        |
| `POST` | `/v1/tasks/:id/confirm`             | Decontezi o sarcină neconfirmată din propriul tău cod.                                                                                                                                                 |

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
# Ce mă așteaptă?
curl https://ai.codai.ro/v1/tasks/pending \
  -H "Authorization: Bearer $CODAI_API_KEY"

# Confirmi unul
curl https://ai.codai.ro/v1/tasks/5c1d…/confirm \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "confirmed", "note": "Testele sunt verzi după patch." }'
```

</Tab>
<Tab value="TypeScript">

```ts
const headers = {
  Authorization: `Bearer ${process.env.CODAI_API_KEY}`,
  'Content-Type': 'application/json',
};

const { tasks } = (await (
  await fetch('https://ai.codai.ro/v1/tasks/pending', { headers })
).json()) as {
  tasks: Array<{ id: string; title: string | null; confirm_deadline_at: string | null }>;
};

for (const t of tasks) {
  await fetch(`https://ai.codai.ro/v1/tasks/${t.id}/confirm`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ outcome: 'confirmed' }),
  });
}
```

</Tab>
<Tab value="Python">

```python
headers = {"Authorization": f"Bearer {os.environ['CODAI_API_KEY']}"}

pending = requests.get("https://ai.codai.ro/v1/tasks/pending", headers=headers, timeout=15).json()["tasks"]
for t in pending:
    requests.post(
        f"https://ai.codai.ro/v1/tasks/{t['id']}/confirm",
        headers=headers,
        json={"outcome": "confirmed"},
        timeout=15,
    )
```

</Tab>
</Tabs>

### `POST /v1/tasks/:id/confirm` [#post-v1tasksidconfirm]

Corp: `{ "outcome": "confirmed" | "fail", "note"?: string ≤ 500 }`. Răspuns:

```json
{ "taskId": "5c1d…", "outcome": "confirmed", "billed": true, "refundedMicroEur": 118000 }
```

| Status | `code`                 | Când                                                                                                 |
| ------ | ---------------------- | ---------------------------------------------------------------------------------------------------- |
| `400`  | `bad_request`          | Corp invalid.                                                                                        |
| `403`  | `forbidden`            | Sarcina aparține altui utilizator.                                                                   |
| `404`  | `not_found`            | Id necunoscut.                                                                                       |
| `409`  | `task_not_confirmable` | Fereastra de 24 de ore s-a închis, sau sarcina e deja închisă (`Task is already closed as "pass".`). |

Doar sarcinile `open` și `unconfirmed` acceptă o confirmare. Hub-ul folosește aceleași reguli; cine răspunde primul câștigă, iar celălalt primește `409`.
