Files
paperclip-docs/docs/reference/api/costs.md
Aron PrinsandClaude Opus 5 325c9eb819 seo: hand-write titles and descriptions for all 192 pages (#115)
## Why

The build derived `<title>` from the **sidebar label** and the
description from the **first paragraph clipped to 220 characters**. Both
are the wrong source. A sidebar label is written for a 240px-wide nav
that supplies its own context; a search result has none.

On production that produced:

- **7 pages titled "Overview"**, and 32 pages sharing a title with
another page
- **109 of 193 descriptions cut off mid-word**, and 174 over 160
characters (where Google truncates)

```
/guides/welcome/key-concepts/
  BEFORE (220):  ...and if you misunderstand them early, the product wi
```

Near-identical titles are the weak-differentiation signal behind
"crawled — currently not indexed", which is the exact symptom on this
property.

## What changed

`seo_title` and `seo_description` frontmatter on **every one of the 192
pages**, written by hand against each page's real content and optimised
for the click — lead with what the reader can do, name the concrete
things covered, no keyword stuffing, no repeating the title.

| | Before | After |
|---|---|---|
| Duplicate titles | 32 pages | **0** |
| Duplicate descriptions | — | **0** |
| Descriptions cut mid-word | 109 | **0** |
| Descriptions over 160 chars | 174 | **0** |
| Title length (incl. suffix) | up to "Overview" | **26–60** |
| Description length | 31–240 | **122–158** |

**No prose was rewritten.** I measured content overlap between the
colliding pairs first — only 17–23%, so these were always distinct pages
wearing the same label, not duplicate content. The `docs/` diff is **564
insertions, zero deletions**.

### Examples

```
BEFORE — all seven identical:  "Overview | Paperclip Docs"
AFTER:  "Adapters Overview"      "API Overview"        "CLI Overview"
        "Deployment Overview"    "Bundled Skills"      "Optional Skills"
        "Experimental Features"

BEFORE — both identical:  "Issues | Paperclip Docs"
AFTER:  "Issues: Creating and Tracking Work"   /guides/day-to-day/issues/
        "Issues API"                           /reference/api/issues/
```

```
/guides/getting-started/five-minute-path/
BEFORE (220):  The shortest route from a running Paperclip instance to an agent that has
               just completed work for you. After Paperclip is installed, the path takes
               about 5 minutes: create a company, hire a CEO agent, approve its first s
AFTER  (131):  Install Paperclip, create a company, hire a CEO agent, and approve its
               first strategy — a working AI company in about five minutes.
```

## Plumbing

- **`site/build-release.mjs`** prefers the authored fields; the derived
path stays only as a fallback the CI gate prevents ever reaching.
`<title>`, `<meta name="description">`, and the `TechArticle` JSON-LD
all read from the same source.
- **`site/app.js`** uses the same fields, so a client-side transition
cannot leave a different title on screen than the crawler was served.
Verified in-browser: navigating `/guides/day-to-day/issues/` →
`/reference/api/issues/` swaps to exactly the server-rendered values, no
JS errors.
- **`scripts/verify-seo-metadata.mjs`** (new) enforces presence,
uniqueness, length, complete-sentence endings, and the
frontmatter-safety rules. Wired into `docs:test` and CI as its own step.

I negative-tested the gate rather than assuming it works — it correctly
fails on a duplicate title, a mid-word description, and a missing field,
and passes again once restored.

## Automating it going forward

`skills/sync-docs/SKILL.md` now:

- gains an **"Authored SEO metadata"** section: why the fields exist,
the enforced rules as a table, and how to actually write them (title
names the subject not the nav position; description writes for the
click; never generate mechanically from the first paragraph — *that is
the fallback the gate exists to prevent*).
- requires both fields in the **page-authoring instruction** given to
every subagent, with guidance on when to update an existing description
versus leave it.
- runs `docs:test:seo-metadata` and `docs:test:crawlable-links` as
**fail-loud gates in Phase 7** before commit.
- documents all six build gates in the files/gates section.

So once this lands, a new page cannot ship without hand-written
metadata, and a title collision cannot reach production.

## Verified

All six verifiers pass:

```
Static route verification passed
Asset fingerprint verification passed
Hierarchical Skills nav verification passed
Skill source block verification passed (11 skill pages, 1 control page)
Crawlable-link contract passed: 40886 internal links across 194 pages resolve in one hop
Authored-SEO contract passed: 192 pages carry a unique, hand-written seo_title and seo_description
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-08-25 22:51:42 +02:00

389 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
paperclip_version: v2026.720.0
seo_title: Costs API
seo_description: Company-scoped endpoints for the four spend questions: what was spent, by whom, against which budget, and how close the company is to its cap.
---
# Costs
Use this API when you want to answer four questions:
- How much did the company spend?
- Which agents, models, providers, or projects are driving that spend?
- Are budgets about to warn or hard-stop work?
- What non-token financial events were recorded alongside token spend?
All of these endpoints are company-scoped.
---
## Reporting Usage
### Cost events
`POST /api/companies/{companyId}/cost-events`
This is the main endpoint for token spend. Adapters typically call it after a heartbeat or other AI operation finishes.
Required fields:
- `agentId`
- `provider`
- `model`
- `costCents`
- `occurredAt`
Common optional fields:
- `issueId` when the spend came from a specific task
- `projectId` when the spend clearly belongs to one project
- `goalId` when you can attribute the spend higher up the tree
- `heartbeatRunId` when the spend came from a specific run
- `billingCode` for your own accounting label
- `biller` if the charge came from a billing entity different from `provider`
- `billingType` if you want to distinguish `metered_api`, `subscription_included`, `subscription_overage`, `credits`, `fixed`, or `unknown`
- `costStatus` to mark whether the usage is priced — `reported` for normal spend, or `unpriced` when you recorded token usage but no dollar cost was available
- `inputTokens`, `cachedInputTokens`, `outputTokens` for token-level reporting
Rules from the implementation:
- The agent must belong to the company.
- Board users can report any company agent’s costs.
- Agent-authenticated calls can only report that agent’s own costs.
- `biller` defaults to `provider` when omitted.
- `billingType` defaults to `unknown` when omitted.
- `costStatus` defaults to `reported` when omitted.
- `occurredAt` must be an ISO datetime string.
> **Priced vs. unpriced usage.** Most cost events carry a real `costCents` amount, so they stay `reported`. When an agent run uses tokens but Paperclip can't determine a dollar cost — for example a local CLI whose per-token pricing isn't known — the usage is recorded with `costStatus` set to `unpriced` instead of being logged as if it cost nothing. That keeps the token counts on the record while flagging that the money figure is an unknown rather than a real zero.
When the event is accepted, Paperclip:
- stores the event
- recalculates `spentMonthlyCents` for the agent and company
- evaluates budget policies for soft warnings and hard stops
- writes an activity log entry
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -X POST "http://localhost:3100/api/companies/company-1/cost-events" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"agentId": "agent-1",
"issueId": "issue-1",
"projectId": "project-1",
"heartbeatRunId": "run-1",
"provider": "anthropic",
"biller": "anthropic",
"billingType": "metered_api",
"model": "claude-sonnet-4-20250514",
"inputTokens": 15000,
"cachedInputTokens": 2000,
"outputTokens": 3000,
"costCents": 12,
"occurredAt": "2026-04-15T12:30:00.000Z"
}'
```
<!-- tab: JavaScript -->
```js
await fetch("http://localhost:3100/api/companies/company-1/cost-events", {
method: "POST",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: "agent-1",
projectId: "project-1",
heartbeatRunId: "run-1",
provider: "anthropic",
billingType: "metered_api",
model: "claude-sonnet-4-20250514",
inputTokens: 15000,
cachedInputTokens: 2000,
outputTokens: 3000,
costCents: 12,
occurredAt: "2026-04-15T12:30:00.000Z",
}),
});
```
<!-- tab: Python -->
```python
import requests
requests.post(
"http://localhost:3100/api/companies/company-1/cost-events",
headers={"Authorization": "Bearer <token>"},
json={
"agentId": "agent-1",
"projectId": "project-1",
"heartbeatRunId": "run-1",
"provider": "anthropic",
"billingType": "metered_api",
"model": "claude-sonnet-4-20250514",
"inputTokens": 15000,
"cachedInputTokens": 2000,
"outputTokens": 3000,
"costCents": 12,
"occurredAt": "2026-04-15T12:30:00.000Z",
},
)
```
<!-- /tabs -->
> **Tip:** If you already know the issue, project, or run that caused the spend, send those IDs. They make the breakdown views much more useful later.
### Cache-adjusted cost
Prompt caching means the amount a provider actually bills for a run is often well below what its raw token counts imply. Paperclip records the billed amount rather than the nominal one, so the dollars in the ledger match the dollars on your invoice.
Adapters express this through their execution result:
- `costUsd` — the cost the adapter reports for the run, in US dollars.
- `cacheAdjustedCostUsd` — the provider-billed cost after prompt-cache discounts, in US dollars. Adapters set this when they expose it separately from `costUsd`.
Paperclip resolves the two into a single billed figure:
- If `cacheAdjustedCostUsd` is a finite number that is zero or greater, that value is the billed cost.
- Otherwise, if `costUsd` is a finite number that is zero or greater, that value is the billed cost — a provider-reported `costUsd` is treated as already cache-adjusted.
- Otherwise there is no billed cost for the run.
That resolved figure is what Paperclip converts into the `costCents` of the cost event it records for the run, so it is the number that flows into every summary, breakdown, and budget policy described below. It is also the figure used to decide `costStatus`: a run with token usage but no resolvable cost is recorded as `unpriced` rather than as a real zero.
The run keeps both numbers. The heartbeat run's `usageJson` carries `costUsd` when the adapter reported one and `cacheAdjustedCostUsd` when a cache-adjusted amount was resolved, alongside the token counts and the `provider`, `biller`, `model`, `costStatus`, and `billingType` fields. Read them from either of:
- `GET /api/companies/{companyId}/heartbeat-runs`
- `GET /api/heartbeat-runs/{runId}`
> **Note:** `cacheAdjustedCostUsd` and `costUsd` are in dollars, while `costCents` on cost events is in cents. Don't mix the two units when you reconcile a run against its ledger entry.
---
## Reading Spend
### Company summary
`GET /api/companies/{companyId}/costs/summary`
This returns the company’s total spend for the selected date range, the current company budget, and the utilization percentage.
If you omit `from` and `to`, the service returns all-time totals.
Query parameters:
- `from` - optional ISO datetime
- `to` - optional ISO datetime
Response fields:
- `spendCents`
- `budgetCents`
- `utilizationPercent`
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl "http://localhost:3100/api/companies/company-1/costs/summary?from=2026-04-01T00:00:00.000Z&to=2026-04-30T23:59:59.999Z" \
-H "Authorization: Bearer <token>"
```
<!-- tab: JavaScript -->
```js
const res = await fetch(
"http://localhost:3100/api/companies/company-1/costs/summary?from=2026-04-01T00:00:00.000Z&to=2026-04-30T23:59:59.999Z",
{ headers: { Authorization: "Bearer <token>" } },
);
const summary = await res.json();
```
<!-- tab: Python -->
```python
import requests
summary = requests.get(
"http://localhost:3100/api/companies/company-1/costs/summary",
headers={"Authorization": "Bearer <token>"},
params={
"from": "2026-04-01T00:00:00.000Z",
"to": "2026-04-30T23:59:59.999Z",
},
).json()
```
<!-- /tabs -->
### Breakdown views
These endpoints use the same optional `from` and `to` filters as the summary endpoint:
- `GET /api/companies/{companyId}/costs/by-agent`
- `GET /api/companies/{companyId}/costs/by-agent-model`
- `GET /api/companies/{companyId}/costs/by-provider`
- `GET /api/companies/{companyId}/costs/by-biller`
- `GET /api/companies/{companyId}/costs/by-project`
What each one is for:
- `by-agent` helps you find which employees are expensive overall.
- `by-agent-model` helps you spot a specific agent/model combination that is burning tokens.
- `by-provider` helps you compare Anthropic, OpenAI, and any other provider you ingest.
- `by-biller` helps when the provider name and the billable entity are not the same.
- `by-project` helps you connect cost back to project work rather than just an agent.
### Fast moving spend
`GET /api/companies/{companyId}/costs/window-spend`
This returns rolling spend for the last `5h`, `24h`, and `7d`. It is useful for spotting sudden spikes without having to choose a date range yourself.
---
## Budget Controls
### Set company budget
`PATCH /api/companies/{companyId}/budgets`
This is the simplest way to set the monthly company budget. The implementation also syncs a matching company budget policy behind the scenes.
### Set agent budget
`PATCH /api/agents/{agentId}/budgets`
This sets the monthly budget for a specific agent and also syncs the corresponding agent budget policy.
The request body for both endpoints is the same:
```json
{ "budgetMonthlyCents": 5000 }
```
The budget window is calendar-month UTC for company and agent monthly budgets.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -X PATCH "http://localhost:3100/api/companies/company-1/budgets" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "budgetMonthlyCents": 100000 }'
```
<!-- tab: JavaScript -->
```js
await fetch("http://localhost:3100/api/companies/company-1/budgets", {
method: "PATCH",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
},
body: JSON.stringify({ budgetMonthlyCents: 100000 }),
});
```
<!-- tab: Python -->
```python
import requests
requests.patch(
"http://localhost:3100/api/companies/company-1/budgets",
headers={"Authorization": "Bearer <token>"},
json={"budgetMonthlyCents": 100000},
)
```
<!-- /tabs -->
### Budget overview
`GET /api/companies/{companyId}/budgets/overview`
Use this when you want the board-level view of budget health. It returns:
- the current policies
- active incidents
- paused agent and project counts
- pending approval count
### Policy upsert
`POST /api/companies/{companyId}/budgets/policies`
This is the general budget policy API. Use it when you need a policy for a company, agent, or project rather than just a monthly company budget.
Important defaults from the implementation:
- `metric` defaults to `billed_cents`
- `windowKind` defaults to `calendar_month_utc` for company and agent scopes
- `windowKind` defaults to `lifetime` for project scopes
- `warnPercent` defaults to `80`
- `hardStopEnabled` defaults to `true`
- `notifyEnabled` defaults to `true`
- `isActive` defaults to `true`
### Budget incidents
`POST /api/companies/{companyId}/budget-incidents/{incidentId}/resolve`
Allowed actions:
- `keep_paused`
- `raise_budget_and_resume`
If you raise the budget, you must supply a new `amount` that exceeds the current observed spend.
> **Warning:** Hard-stop budget enforcement pauses the affected scope and cancels work for that scope. A budget increase only resumes the scope if the new budget is above current observed spend.
### What happens at the thresholds
- At the warn threshold, the service creates a soft incident when notifications are enabled.
- At 100 percent, the service creates a hard incident when hard-stop is enabled.
- Hard-stop policies pause the affected company, agent, or project and cancel work for that scope.
- Budget windows reset on the first day of each month in UTC.
- Project policies default to lifetime windows unless you choose a different window kind explicitly.
---
## Finance Events
These endpoints track non-token accounting events alongside cost events. They are useful for credits, refunds, platform fees, manual adjustments, and other finance records that are not raw token usage.
### Create finance event
`POST /api/companies/{companyId}/finance-events`
This endpoint is board-only.
### Read finance data
- `GET /api/companies/{companyId}/costs/finance-summary`
- `GET /api/companies/{companyId}/costs/finance-by-biller`
- `GET /api/companies/{companyId}/costs/finance-by-kind`
- `GET /api/companies/{companyId}/costs/finance-events`
The finance list endpoint supports:
- `from`
- `to`
- `limit` from `1` to `500` with a default of `100`
Useful event kinds include things like `inference_charge`, `platform_fee`, `credit_purchase`, `credit_refund`, and `manual_adjustment`. The API also stores `direction`, `amountCents`, `currency`, `estimated`, and optional metadata.
---
## Internal Diagnostics
`GET /api/companies/{companyId}/costs/quota-windows`
This is a board-only diagnostic endpoint. It returns quota window information used by the system to understand provider usage windows. The company must exist and the caller must be board-authenticated.
---
## Practical Reading Order
If you are trying to understand a company’s spending, start here:
1. `GET /costs/summary`
2. `GET /costs/by-agent`
3. `GET /costs/by-project`
4. `GET /budgets/overview`
5. `GET /costs/window-spend`
That sequence usually tells you whether the problem is a single agent, a single project, a provider mix issue, or a real budget policy problem.