---
title: "ai.oz0/ai-spend"
description: "What the company spends at Anthropic and OpenAI directly, read from their admin APIs on your own nodes and shown by team beside Orchestrator Zero's own spend."
canonical_url: "https://docs.oz0.ai/build/plugins/verified/ai-spend"
---
# ai.oz0/ai-spend

> What the company spends at Anthropic and OpenAI directly, read from their admin APIs on your own nodes and shown by team beside Orchestrator Zero's own spend.

For: tenant administrators and plugin users.

`ai.oz0/ai-spend` imports company AI spending that did not pass through Orchestrator Zero. It reads Anthropic and OpenAI admin cost and usage reports on your nodes with the organisation's admin keys.

The plugin records [outside spend](https://docs.oz0.ai/spend/cost-and-usage/accounting#outside-spend) by day, provider, model and team alongside gateway spending. Its only outbound requests are to those two admin APIs.

<table>
<thead>
  <tr>
    <th>
      Function
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        oz0_ai_spend.import_spend
      </code>
    </td>
    
    <td>
      <code>
        days
      </code>
      
       (1 to 31, 7 by default) or <code>
        start
      </code>
      
       and <code>
        end
      </code>
      
      , and <code>
        dry_run
      </code>
      
      : reads both providers' cost reports, and their usage reports for tokens, for whole UTC days, maps each workspace or project to a team, and hands the rows to Orchestrator Zero. Never today, which is not over.
    </td>
  </tr>
</tbody>
</table>

The `oz0-ai-spend-import` flow calls it, and the plugin ships a routine that runs the flow once a day for the last
seven days, so a provider's late correction of a recent day is picked up and a day missed while a node was away is
imported the next day. Importing the same days again adds nothing.

## Install

```bash [Terminal]
oz0 plugin install hub:ai.oz0/ai-spend --accept-provides outside-spend
printf '%s' "$ANTHROPIC_ADMIN_KEY" | oz0 secret set AI_SPEND_ANTHROPIC_ADMIN_KEY
printf '%s' "$OPENAI_ADMIN_KEY" | oz0 secret set AI_SPEND_OPENAI_ADMIN_KEY
oz0 setting set AI_SPEND_TEAMS 'anthropic:Research = research; openai:Growth = growth; openai:* = platform'
oz0 routine save -f plugins/ai-spend/routines/oz0-ai-spend-daily.yaml
oz0 routine run ai-spend-daily --wait
oz0 usage outside
```

- `--accept-provides outside-spend` is a tenant admin's grant: the plugin may record spend that belongs to no job.
An install without it is refused and says so. The grant is audited (`plugin.provides.granted`), shown by
`oz0 plugin list`, taken back with `oz0 plugin revoke-provides ai-spend outside-spend`, and dropped when the plugin is
removed. See [the manifest's provides](https://docs.oz0.ai/reference/plugin-manifest/data#provides).
- The admin keys are an Anthropic Admin API key (`sk-ant-admin...`, made by an organization admin in the Claude
Console) and an OpenAI admin key (made in the organization settings). Set one or both; a provider without a key is
skipped. They are tenant secrets bound by [credential injection](https://docs.oz0.ai/admin/credential-injection): the plugin's process
holds placeholders, and the node's egress proxy puts the key on GET requests to `api.anthropic.com/v1/organizations`
and `api.openai.com/v1/organization` alone.
- `AI_SPEND_TEAMS` maps workspaces and projects to teams: one rule per line or separated by `;`,
`<provider>:<workspace or project name or ID> = <team>`, `*` for any. Anthropic's default workspace is `default`. The
first rule that matches wins; spend no rule maps is the tenant's, shown as unassigned.
- The routine in `routines/oz0-ai-spend-daily.yaml` fires at 04:20 UTC with up to 15 minutes of jitter. Set its
`owner_team` to the team that owns spend reporting before you save it.

`AI_SPEND_ANTHROPIC_URL` and `AI_SPEND_OPENAI_URL` point the plugin at a proxy of your own (https only); the keys still
go only to the providers' hosts, so a proxy must forward the request with the injected header to them.

## What it reads, and what it never reads

- The reports are asked **per workspace (Anthropic) or project (OpenAI) and model, never per user, account, service
account or API key**, so no figure about a person ever reaches the plugin. Workspace and project names stay in the
plugin; the rows carry the provider's ID, which no view shows.
- Cost comes from the cost reports only. The usage reports lend input and output tokens to the same day, workspace
and model; no amount is estimated from tokens and a price list.
- Currencies are kept as the provider gives them, and never converted.
- A team figure goes through [works-council mode](https://docs.oz0.ai/govern/works-council-mode): a team of fewer people than the
threshold is summed into one row that names no team.

## When it fails

<table>
<thead>
  <tr>
    <th>
      What it says
    </th>
    
    <th>
      Why, and what to do
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        the anthropic admin key was refused (401)
      </code>
    </td>
    
    <td>
      The key expired, was revoked or is not an admin key: set a new one with <code>
        oz0 secret set AI_SPEND_ANTHROPIC_ADMIN_KEY
      </code>
      
      . The other provider's rows are still imported.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tenant ... has not granted plugin ai-spend outside-spend
      </code>
    </td>
    
    <td>
      The grant was revoked or never given: install again with <code>
        --accept-provides outside-spend
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ... is not on the egress allowlist
      </code>
    </td>
    
    <td>
      <code>
        AI_SPEND_*_URL
      </code>
      
       names a host that is neither the provider's nor on the tenant's list: allow it with <code>
        oz0 tenant egress
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        more than 500 rows; import fewer days at a time
      </code>
    </td>
    
    <td>
      One call carries at most 500 rows: import a shorter range (<code>
        days
      </code>
      
      , or <code>
        start
      </code>
      
       and <code>
        end
      </code>
      
      ).
    </td>
  </tr>
</tbody>
</table>

A fire that imports nothing fails, so a routine that keeps failing pauses itself and says why
([routines](https://docs.oz0.ai/build/parts/routines)).

## Tests

`make test-plugins` reads the providers' admin APIs from `scripts/fakellm` (paging, amounts, currencies, partial days,
the mapping, an expired key, and that nothing is asked per person), and `scripts/e2e-examples.sh` installs the plugin
with its keys injected, runs its routine twice against the fakes and checks the rows by team.

## The import asks by default

The plugin's suggested policy asks a person before each `import_spend` ("the plugin asks before it writes the
tenant's outside spend"). The daily routine runs unattended, so a tenant that saves it switches the function once, with
a reason: `oz0 plugin policy ai-spend --set oz0_ai_spend.import_spend=allow --reason "the daily import runs unattended"`.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
