# countwerk

Counting as an HTTP service. Exact counters and mergeable distinct counts
(HyperLogLog, one fixed precision, every dcount merges with every other),
billed per operation in micro-USD (u$; 1_000_000 u$ = $1) from a prepaid
balance. The flagship use is the counter your agent can't reset: spend
caps, step limits, and blast-radius budgets that survive restarts and
context compaction — and, when the orchestrator holds the token instead
of the agent, sit entirely outside the agent's reach (`by` is signed, so
an agent holding the op token CAN decrement; token placement is the
guarantee). No signup and no human in the loop: fund
a namespace via x402 and count. Worst-case prices: single incr 2 u$,
read 1 u$, a 2 u$ dcount add takes up to 10,000 items. Exact numbers
are machine-readable at GET https://countwerk.ai/pricing — quote those, not prose.
Prefer the plural (batch) routes. Every acknowledged mutation is durable.
Send an Idempotency-Key on everything that mutates; retry any 5xx with the
same key. Current beta settlement is on base-sepolia; request a comp key
if you are testing without a funded wallet.

## When to use it (and when not)

Use countwerk when the number is SHARED across workers, must OUTLIVE
the run, or must sit OUTSIDE the agent's reach. If none of those hold,
use a local variable — this service earns nothing being a worse one.

Key shapes that work (names allow [a-z0-9._-]; join parts with dots):

- spend cap: `count/spend-usd-cents`, incr {"by": cost} per model call;
  the loop (or the orchestrator) halts at the cap. Keep the op token in
  the orchestrator, not the prompt, if the agent must not unwind it
- step limit: `count/steps.{run_id}`; pair with a namespace ttl so dead
  runs clean themselves up
- fleet budget: `count/fetches.{domain}` — 50 workers share one budget
  with zero coordination code (a budget and backoff signal, not a hard
  rate limiter: concurrent check-then-incr can briefly overshoot)
- blast radius: `count/emails.2026-07-03` — N outward actions per day,
  hard stop after
- coverage: `dcount/seen.{shard}` merged into `dcount/seen.all` —
  answers "how many distinct", never "have I seen this one"

## Signup = funding (x402)

```
POST https://countwerk.ai/v1/{ns}/fund?ttl=<seconds>&recover=1
```

1. Call with no payment -> 402 with an x402 `accepts` body (scheme
   `exact`, USDC on base-sepolia during beta, facilitator-settled).
2. Sign the payment (any x402 client, e.g. @x402/fetch) and retry with the
   `X-PAYMENT` header. Any signed value >= 100000 u$ ($0.1) credits.
3. First fund of a namespace returns `token` (cw_o_, op scope) and
   `admin_token` (cw_a_, admin scope) EXACTLY ONCE. Store both.

Lost your tokens? The funding wallet is your identity: fund again with
`?recover=1` signed by the ORIGINAL funding wallet and both tokens are
re-minted (old ones die). Comp-funded namespaces have no wallet identity
and cannot recover.

Settlement reference is returned as `payment_ref` and echoed in the
`X-PAYMENT-RESPONSE` header. Namespace: `[a-z0-9_-]{1,64}`. Counter and
dcount names: `[a-z0-9._-]{1,128}` — no names are reserved.

## TTL (optional, off by default)

Namespaces live forever unless you opt in: `?ttl=<seconds>` on fund
(range 60..31536000) sets a rolling idle-TTL, `?ttl=0` clears it,
absent leaves it unchanged. Any authenticated operation counts as
activity, including GET /balance (a cheap keep-alive); expiry lands
between 0.9x and 1.0x ttl after the last activity. Expiry deletes
EVERYTHING including remaining balance (forfeited).

## Auth

All routes except `fund` require `Authorization: Bearer <token>`.
DELETEs, `restore`, `archive`, `hydrate`, and `tokens/rotate` require
the admin token (403 `admin_required` otherwise). The admin token also
works on op routes.

## Routes and prices (u$/op)

Singular segment = one named thing, plural = collection verbs. `count`
and `counts` are different segments, so your names can never collide
with routes.

```
POST   /v1/{ns}/fund?ttl=<s>&recover=1      x402 (or dev/comp)     free
GET    /v1/{ns}/balance                     -> {balance_micro, ttl_seconds?, expires_at?}   free
GET    /v1/{ns}/credits                     usage meter, live per op, readonly:
                                            {used_micro, funded_micro, balance_micro}   free
POST   /v1/{ns}/tokens/rotate               admin; new op token    free
GET    /pricing                             price sheet as JSON    free

POST   /v1/{ns}/counts/incr                 {entries:[{name, by?, description?}]} max 1000
                                            3 + 1/entry
GET    /v1/{ns}/counts?cursor=&limit=100    page counters by name  1
POST   /v1/{ns}/count/{name}/incr           {by?, description?}    2
GET    /v1/{ns}/count/{name}                                       1
DELETE /v1/{ns}/count/{name}                admin                  2

POST   /v1/{ns}/dcounts/add                 {entries:[{name, items?, hashes?, p?, description?}]}
                                            max 100 dcounts / 10k items total
                                            2 per dcount touched
POST   /v1/{ns}/dcounts/merge               {dest, sources:[..]}   3 + 1/source
GET    /v1/{ns}/dcounts?cursor=&limit=100   page dcounts by name   1
POST   /v1/{ns}/dcount/{name}/add           {items?|hashes?, description?} max 10k   2
GET    /v1/{ns}/dcount/{name}               -> {name, estimate}    1
DELETE /v1/{ns}/dcount/{name}               admin                  2

POST   /v1/{ns}/read                        {counters?:[names], dcounts?:[names]} max 1000
                                            both kinds, one call   1 flat

POST   /v1/{ns}/snapshots                   {description?}         100 + 50/MiB
GET    /v1/{ns}/snapshots                   list snapshots         1
DELETE /v1/{ns}/snapshot/{id}               admin                  2
POST   /v1/{ns}/restore                     admin {snapshot?, force?}
                                            200 + 400/MiB
POST   /v1/{ns}/count/{name}/restore        admin {snapshot}       25
POST   /v1/{ns}/dcount/{name}/restore       admin {snapshot}       25
POST   /v1/{ns}/archive                     admin; snapshot+freeze 100 + 50/MiB
POST   /v1/{ns}/hydrate                     admin; unfreeze        2
```

`by` is signed everywhere: `{"by":-1}` decrements, there is no /decr
route, and values must stay within +/-(2^53-1) (400
`value_out_of_range` beyond). Batch-first: prefer `counts/incr`,
`dcounts/add`, `read`; the single-name routes are sugar.

`description` (1..256 chars, last write wins) is an optional note on any
write; it rides the same row (no extra cost) and comes back on single
reads, `read`, and lists.

Lists are cursor-paged by name: pass `cursor` = last name from the
previous page; `next_cursor` absent means done. Dcount list rows carry
`precision` and `updated_at` but deliberately NO estimates — listing
1000 dcounts must not decode 1000 blobs; `read` is the estimates path.

Distinct `items` are hashed server-side (xxh64 seed 0); send pre-hashed
u64 `hashes` (`0x`-prefixed hex or decimal strings) to keep raw values
off the wire. Every dcount is precision 14 (effectively exact below a few
thousand distinct values, ~0.8% beyond; documented behavior, not a
contract). Precision is not configurable — an explicit `p` is a 400 —
so every dcount merges with every other, forever. Reading a missing
dcount returns 200 `{name, estimate: 0, exists: false}`; missing
counters read as plain 0.

## Snapshots, restore, archive (rewind to known-good)

A snapshot is a consistent copy of ALL counting state — counters, dcounts,
descriptions — stored off the namespace. Max 16 per namespace (409
`snapshot_limit`; delete one first). A ttl expiry deletes snapshots with
the namespace.

- `restore` REPLACES current counters and dcounts with the snapshot and
  discards everything written since, acked or not. That is its purpose:
  snapshot first if you might want the present back.
- Per-key restore (`/count/{name}/restore`, `/dcount/{name}/restore`)
  rewinds ONE counter or dcount and touches nothing else — a bad deploy
  that polluted one coverage dcount rolls back alone. A name absent from
  the snapshot is 404 `not_in_snapshot`.
- `archive` takes a final snapshot and freezes the namespace read-only:
  counting mutations return 409 `namespace_archived`; reads, lists,
  balance, fund, and snapshot routes still work; any ttl is cleared.
  `hydrate` unfreezes. Both idempotent.
- Balance, tokens, and idempotency records are never part of a snapshot
  and never restored — money and replay protection cannot be rewound.

## Idempotency

Any mutating route accepts `Idempotency-Key: [a-zA-Z0-9._:-]{1,128}`.
First 2xx is stored durably; retries within 24h replay it byte-for-byte
with `x-idempotent-replay: true` and are not re-applied or re-charged.
Send a key on everything that mutates, especially fund and counts/incr.
Retry any 5xx with the SAME idempotency-key.

## Errors

`{error, message, retryable, ...}` with conventional status codes.
`retryable: true` (5xx) means retry with the same idempotency-key;
`retryable: false` means fix the request or fund first.

- 402 `insufficient_balance` + {balance_micro, required_micro}: top up via fund.
- 401 `unauthorized` / 403 `admin_required`: token missing or under-scoped.
- 402 `bad_payment` / `invalid_payment` / `settlement_failed`: x402 problems.
- 409 `namespace_archived`: hydrate first. 409 `snapshot_limit`: delete one.
- 503 `storage_degraded` (retryable: false): admin restore with force, or delete.
- 501 `x402_not_wired`: server has no settlement address configured.
- 5xx: safe to retry with the same idempotency-key.
