# satoshis.watch Agent API (v1)

An account owner creates an API key in **Account → gear → API access**, chooses whether that key can read or read and write, then sets access on individual Account cards and Spaces. API paths still say `portfolio` for Account cards and `bookmark-spaces` for Spaces. Give the key to an AI in its secure settings; the full key is shown only once. Do not put it in a URL or a browser page.

Base URL: `https://satoshis.watch/api/watch/agent/v1`  
Authentication: `Authorization: Bearer <API_KEY>`  
JSON requests: `Content-Type: application/json`

Machine-readable OpenAPI description: `https://satoshis.watch/api/watch/agent/v1/openapi.json`. It describes only the Agent API; send your key on authenticated operations, never when fetching the description.

The API key is separate from the Watch Key and cannot sign in to an owner session, change permissions, manage other keys, or change account or billing settings. A write key can create and delete bookmark spaces under the rules below. A key stays valid until revoked. Password changes, Watch Key rotation, and account deletion revoke all API keys; ordinary sign-out does not.

## Access model

Sheet Spaces use the same per-Space `none` / `read` / `write` grant. They expose five text columns and 1,000 fixed row slots through `GET` and `PATCH /bookmark-spaces/<SPACE_ID>/sheet`, with `expected_revision` on writes. The first four column headings are locked. The fifth defaults to `Notes` and is the only heading that can be renamed; existing Sheets may have a custom fifth heading such as `Numbers`. Read the current `headers` before writing. A write-capable Agent can create a new Sheet Space when Watch+ has room; an owner must explicitly grant access to an existing Sheet. See the [Sheet API guide](https://satoshis.watch/api/watch/agent/v1/docs/sheets) for the row, filter, upsert, delete, compaction, heading, and write contract.

A key's `read` or `write` capability is its maximum permission. A read-only key cannot create Portfolio cards or make any other change, even where a resource has `write` access. All keys share the card and space grants, but a write-capable key can change a resource only when that resource also has `write` access. `effective_access` in Agent responses reflects both the key capability and the resource grant, so a read-only key sees `read` for a writable resource. Revoke a key and create another to change its capability.

For resource grants, `none` hides the resource and its saved metadata. `read` permits viewing it. `write` permits viewing, creating cards in that space, editing/deleting cards and notes, and moving cards when the source and destination rules allow. Bookmark cards inherit their whole space's level; they have no individual grant. Existing spaces start at `none`; an Agent-created space starts at `write`. Human-created or human-moved Portfolio cards start at `none`; cards genuinely created or moved into Portfolio by a write-capable API key start at `write`.

An Account card's grant controls access to that card and its History, including its transactions and notes. A write-capable key may create a **new** Portfolio card when the account has space under its current Watch/Watch+ card limit, even when it cannot see existing cards. The new card receives `write` access. A duplicate address is rejected rather than claiming access to the existing card; a full Account returns `OVERVIEW_FULL`. Portfolio and Bitcoin spaces require Bitcoin addresses. Alt Coin spaces accept a selected coin and validated address (or `unknown` with safe text); the existing coin-type move restrictions still apply. Watch includes one Bookmark space with up to ten cards. Watch+ includes up to ten spaces with ten cards each. After a downgrade, only the first stored space is accessible; extra spaces and their cards are preserved for renewal. The first Watch space starts as an empty Bitcoin Page 1 space; the owner can grant API access to it.

Space-to-space moves require `write` on both spaces. Portfolio-to-space moves require `write` on the source card and destination space. Space-to-Portfolio moves require `write` on the source space and assign `write` to the resulting Portfolio card. A card moved into a space inherits that destination space's access. An owner changing a grant takes effect on the next request; writes use a current version and fail on a stale grant or edit.

Card and Space responses include only accessible saved cards and notes. `GET /portfolio/cards` includes `scope: "accessible_portfolio"` and `included_card_ids`; this is the exact card set in that response. `GET /me` shows the key's capability, accessible card and Space IDs, Account card quota as `portfolio_cards.used`/`limit`, and account-wide stored Space quota as `bookmark_spaces.used`/`limit`. The counts include resources with No access, but `/me` does not reveal those resources' IDs, names, addresses, or notes. After a Watch+ downgrade, stored Space `used` may exceed the current Watch `limit`; only the first Space is accessible. `/me.rate_limit.remaining` is `null` because the nginx counter is not exposed to the app. History responses include only transactions and notes belonging to cards currently accessible through their individual grants. Other purchased data remains outside this API.

## Read

Use your agent runtime's secure secret store to construct the Authorization header in memory. For a local shell, the example below requires Python's [keyring](https://keyring.readthedocs.io/en/latest/) configured with a secure OS backend such as macOS Keychain, Windows Credential Locker, or Linux Secret Service. Do not use a plaintext keyring backend. Enter the key at the interactive prompt, never as a command argument:

```sh
python3 -m keyring set satoshis.watch-agent-api watch-agent
```

Define this helper once. It reads the stored key in memory and passes the header to curl through stdin, keeping the key out of shell history, process arguments, environment variables, and temporary files. Curl must support `--header @-` (7.55.0 or later). Do not enable curl verbose/trace logging or print the key; use only the authenticated URLs below.

```sh
watch_agent() {
  python3 -c '
import keyring, subprocess, sys
key = keyring.get_password("satoshis.watch-agent-api", "watch-agent")
if not key or "\r" in key or "\n" in key:
    raise SystemExit("A valid Agent API key is required in the secure keyring.")
result = subprocess.run(
    ["curl", "--silent", "--show-error", "--header", "@-", *sys.argv[1:]],
    input="Authorization: Bearer " + key + "\n", text=True,
)
raise SystemExit(result.returncode)
' "$@"
}
```

The helper reserves stdin for the header; pass JSON with the shown `-d` argument rather than `-d @-`. HTTP error bodies still need to be checked as described below. Public docs and OpenAPI requests need no key; fetch those with ordinary curl.

```sh
watch_agent 'https://satoshis.watch/api/watch/agent/v1/me'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/portfolio/cards'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/bookmark-spaces'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/bookmark-spaces/<SPACE_ID>'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/bookmark-spaces/<SPACE_ID>/cards'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/bookmark-spaces/<SPACE_ID>/cards/<CARD_ID>'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/history'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/history/cards'
watch_agent 'https://satoshis.watch/api/watch/agent/v1/history/cards/<CARD_ID>/transactions?limit=25&offset=0'
```

The lists return opaque `id` and `version` values for accessible cards and Spaces. Use these values, not a list index or address, for writes. Single-resource GETs wrap the result: `GET /bookmark-spaces/<SPACE_ID>` returns `{"space":{"id":"…","version":"…"}}`; `GET /portfolio/cards/<CARD_ID>` and `GET /bookmark-spaces/<SPACE_ID>/cards/<CARD_ID>` return `{"card":{"id":"…","version":"…"}}`. List routes return arrays under `spaces` or `cards` instead. For a bookmark card create, take `expected_space_version` from the nested `space.version`. An inaccessible or absent card or Space returns `404`; a read-only key attempting a write returns `403`. A trailing slash on a known path redirects with `307` to its canonical path without the slash.

`GET /history` returns a combined summary of cards the Agent can read. `GET /history/cards` lists those cards; ungranted cards and their activity are excluded. Paginate each accessible card's transactions with `limit=1..100`, starting at `offset=0` and passing the returned `next_offset` while `has_more` is true. Watch returns the latest calendar year (inclusive UTC dates), with no page-count limit; `history_export` reports the enforced dates and completeness. If a card has no transactions within that year, its first API page may contain up to 25 older transactions with `history_export.older_preview: true` and `has_more: false`. In the combined History tab, that older preview appears only when all included cards have no recent activity. The preview does not extend the CSV date window. Always use `next_offset`, even for an empty filtered page. Watch+ returns full-history pages, with the existing upstream offset ceiling of 10,000. The response also reports `history_degraded` and `complete`; a degraded upstream result is not a full history. History follows the account's Watch/Watch+ card scope and transaction date window. `total_count` is the upstream lifetime count, not the count within the year. Saved notes are retained when their transactions age outside that window; signed-in CSV downloads include the notes on exported transactions. Read accessible saved notes with `POST /history/notes/batch` and `{"txids":["<64-character-txid>"]}`. The server verifies the transaction's current Portfolio cards before returning a saved note; it omits a note unless every involved card is readable. If transaction details are unavailable, the request fails closed until membership can be verified.

## Create and edit

With a write-capable key and an open Account card slot, create a new Bitcoin Portfolio card; the response includes its `id`, `version`, and `effective_access: "write"`:

```sh
watch_agent -X POST 'https://satoshis.watch/api/watch/agent/v1/portfolio/cards' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 3e57901d-33ea-4f4a-85fa-644f99948466' \
  -d '{"address":"1BoatSLRHtKNngkdXEeobR76b53LETtpyT","name":"Research","note":"Initial note"}'
```

Edit its note with the **latest** version from a read or previous write response:

```sh
watch_agent -X PATCH 'https://satoshis.watch/api/watch/agent/v1/portfolio/cards/<CARD_ID>' \
  -H 'Content-Type: application/json' \
  -d '{"expected_version":"<CARD_VERSION>","note":"Updated research note"}'
```

Create a card in a `write` bookmark space with the latest space version from `GET /bookmark-spaces/<SPACE_ID>/cards`. For an Alt Coin space, set `coin_id` to its supported coin ID; use `unknown` only when no listed coin applies.

```sh
watch_agent -X POST 'https://satoshis.watch/api/watch/agent/v1/bookmark-spaces/<SPACE_ID>/cards' \
  -H 'Content-Type: application/json' \
  -d '{"expected_space_version":"<SPACE_VERSION>","address":"1BoatSLRHtKNngkdXEeobR76b53LETtpyT","name":"Research","note":"Initial note"}'
```

Bookmark card edits use `PATCH /bookmark-spaces/<SPACE_ID>/cards/<CARD_ID>` with `expected_version` and any of `name`, `note`, `color_key`. Portfolio card edits accept the same fields. Do not send an entire bookmark-space snapshot: the server applies only the requested resource change.

A write-capable key can create a bookmark space when the account has Watch+, at least one existing bookmark space has `write` access, and fewer than ten spaces exist. Use the current `bookmark_revision` from `GET /bookmark-spaces`:

```sh
watch_agent -X POST 'https://satoshis.watch/api/watch/agent/v1/bookmark-spaces' \
  -H 'Content-Type: application/json' \
  -d '{"expected_bookmark_revision":"<REVISION>","name":"Research","type":"bitcoin"}'
```

The new Space starts with the account-wide `write` resource grant, visible to all of that account's keys within each key's read/write ceiling. Set `type` to `altcoin` for an Alt Coin Space or `sheet` for a Sheet Space. An owner can change an existing Space's type in the site; this deliberately clears its Agent grant, so the owner must re-grant access in API access. A write-capable Agent can rename a writable Space with `PATCH /bookmark-spaces/<SPACE_ID>` and `{"expected_bookmark_revision":"<REVISION>","name":"New name"}`. The name must be 1–40 printable characters. A stale revision returns `409`. Agent type changes are not supported in v1.

For a readable Sheet, `GET /bookmark-spaces/<SPACE_ID>/sheet?view=sparse` returns nonempty rows in bounded pages. Exact, case-sensitive `address=` and `chain=` filters match the first two cells and imply sparse view. An unsupported `filter=` returns `400`; use `next_offset` for subsequent pages. A write key with Space `write` access can use `PATCH /sheet` for up to 100 indexed rows per request, or these targeted operations with the latest integer `expected_revision` from GET:

- `PUT /bookmark-spaces/<SPACE_ID>/sheet` with `{"expected_revision":0,"cells":["bc1...","Bitcoin","txid","2026-09-29","note"]}` updates the unique Address+Blockchain match or fills the first empty slot. Both key cells must be nonempty; duplicate matches or a full Sheet return `409`.
- `DELETE /bookmark-spaces/<SPACE_ID>/sheet` with `{"expected_revision":0,"index":7}` removes that row and shifts later nonempty rows toward index 0.
- `POST /bookmark-spaces/<SPACE_ID>/sheet` with `{"expected_revision":0}` packs nonempty rows at the start while preserving their order. If already compact, the revision stays unchanged.

Reload row indexes after delete or compact. All Sheet operations use the existing `/sheet` path so nginx's separate Sheet traffic limits apply. Each Sheet has five plain-text columns, 1,000 slots, a 512-character cell limit, and a shared limit of 30 changed Sheet writes per minute per account. These routes do not evaluate formulas. Avoid storing API keys, Watch Keys, passwords, or seed phrases in Sheet cells.

Use `/me.bookmark_spaces.used` and `.limit` before creating a Space. A full plan returns `409 SPACE_FULL`; a full Space returns `409 BOOKMARK_FULL` when adding a card. These codes indicate capacity, not a stale version. A hidden Space still counts toward the plan limit.

With a write-capable key and `write` access to every Account card involved in a transaction, save or replace a note with `PUT /history/notes/<TXID>` and `{"note":"Your note"}`. Delete it with `DELETE /history/notes/<TXID>`. Read-only keys can read History and notes on readable cards but cannot change notes. Notes are account-wide and limited to 50 code points each. Watch can add notes for transactions in the latest calendar year (inclusive UTC dates), transactions currently seen in the mempool, and older transactions on the account's current eligible fallback History page. The server verifies the transaction and fallback page when creating a note; unknown or unavailable dates cannot authorize a new note. Watch+ can add notes on older transactions. Once saved, a note remains readable, editable, and deletable by the owner after its transaction ages out, leaves the displayed page, or the account downgrades. An Agent can do so while its current key and involved card grants still permit that action.

Supported `color_key` values are `amber`, `green`, `blue`, `rose`, `violet`, `teal`, `slate`, `orange`, `cyan`, and `pink`. An unsupported value returns `VALIDATION`. An unknown transaction ID in a note write returns `404 NOT_FOUND`; an upstream verification outage returns `503 HTTP_ERROR`, which can be retried later. Neither case writes a note.

## Move and delete

Move a `write` bookmark card into Portfolio (the resulting Portfolio card receives `write`):

```sh
watch_agent -X POST 'https://satoshis.watch/api/watch/agent/v1/cards/move' \
  -H 'Content-Type: application/json' \
  -d '{"source_kind":"bookmark","source_id":"<BOOKMARK_CARD_ID>","source_space_id":"<SPACE_ID>","destination_kind":"portfolio","expected_source_version":"<CARD_VERSION>"}'
```

For a Portfolio-to-space move, use `source_kind: "portfolio"`, the Portfolio card ID as `source_id`, `destination_kind: "bookmark"`, and `destination_space_id`. Set `expected_source_version` to the source **card's** `version` and `expected_destination_version` to the destination **Space's** `version`. For a space-to-space move, include `source_space_id` and `destination_space_id`; the same two concurrency fields still mean source **card** version and destination **Space** version. There is no source-Space-version field. Read the source card from `GET /bookmark-spaces/<SOURCE_SPACE_ID>/cards/<CARD_ID>` and the destination Space from `GET /bookmark-spaces/<DESTINATION_SPACE_ID>` immediately before moving. A Space-to-Portfolio move needs the source card version and no destination Space version.

A move creates a new card ID at the destination and retires the source ID, including a move between two bookmark spaces. The response includes `old_id`, `new_id`, `location`, and the resulting `card` when it remains accessible. Use `new_id` for the next read or write.

Delete an accessible `write` card with its current version:

```sh
watch_agent -X DELETE 'https://satoshis.watch/api/watch/agent/v1/portfolio/cards/<CARD_ID>' \
  -H 'Content-Type: application/json' \
  -d '{"expected_version":"<CARD_VERSION>"}'
```

Bookmark deletion uses `DELETE /bookmark-spaces/<SPACE_ID>/cards/<CARD_ID>` and the same body. A stale version returns `409`, with no partial change. Reload the card or space before retrying. Replaying a creation cannot turn a protected existing card into an accessible one. Revoke a key from the owner's Agent access panel to stop further requests.

To delete a `write` bookmark space **and all its cards**, send `DELETE /bookmark-spaces/<SPACE_ID>` with the latest `bookmark_revision` from `GET /bookmark-spaces`, the space's `version`, and `"confirm_delete_cards":true`. At least one bookmark space must remain. The response reports `deleted_card_count`. A stale revision or version returns `409` without deleting anything. A Sheet Space must have no stored rows before deletion; a custom fifth heading does not block a confirmed delete. See the [Sheet API guide](https://satoshis.watch/api/watch/agent/v1/docs/sheets) for clearing rows.

## Safe retries

The optional `Idempotency-Key` header works on Portfolio and bookmark card creation, bookmark-space creation and deletion, moves, and both card deletion routes. Generate a fresh 16–128 character token for each intended operation; use only letters, digits, `.`, `_`, `~`, or `-`. Reuse that token only when retrying the **same** request. For 24 hours, the same API key, token, HTTP method, path, and canonical JSON body return the prior logical result without applying the change twice. A different request using that token with the same API key returns HTTP `409` with code `IDEMPOTENCY_CONFLICT`. Tokens are separate across API keys.

Access is checked again on replay. If the owner has since removed access to the resulting card or space, a retry will not return its saved details. Without this header, a retry is a new request and may encounter a duplicate or stale-version error.

## Validate without changing cards

A write-capable key can submit the same JSON body to `POST /portfolio/cards/validate`, `POST /bookmark-spaces/<SPACE_ID>/cards/validate`, or `POST /cards/move/validate`. A valid request returns `{"valid":true}`; a blocked request returns the same status and machine error code used by the corresponding write. Validation does not create, move, or delete a card or use Portfolio capacity. It does count toward the shared request rate limit. Before bookmark validation or a write, re-`GET /bookmark-spaces/<SPACE_ID>` and use its current `version` as `expected_space_version`. A `409 STALE_VERSION` means the space changed after that read; refresh and retry the intended operation.

## Poll for changes

`GET /events` returns a scoped, read-only change feed for the past 24 hours by default. Pass its opaque `next_since` value back as `?since=<NEXT_SINCE>` to continue; an RFC 3339 timestamp with timezone is also accepted for an initial poll. Use `limit=1` through `100` to bound a page and continue while `has_more` is true. Each event has an opaque `id`, `at` time, and one of `portfolio_changed`, `bookmark_changed`, `cards_changed`, or `scope_changed`. Re-fetch `/me` after `scope_changed`; re-fetch the relevant accessible lists after a card change. A read-only key can poll.

For each poll, start with the saved `next_since` cursor (or omit `since` for the first poll), process the returned `events`, then save the new `next_since`. If `has_more` is true, fetch the next page immediately. When `events` is empty and `has_more` is false, stop and use the saved cursor on the next scheduled poll. The cursor can advance across audit records that are outside the key's current scope.

The feed is an advisory invalidation signal from account audit records. It does not include card contents or IDs. Updates and creations from other keys or the owner are filtered using current grants. A key can still receive its own earlier change signal after a grant is removed. Agent deletions and moves emit generic change signals because their sources were granted when the action succeeded; owner deletions and moves do so when a granted resource was involved. Owner bookmark edits appear when a changed space is currently granted. Changes to cards that are now hidden may be omitted. Audit storage can also fail without blocking a card write, so use occasional full reads when correctness matters. Webhooks are not part of v1.

## Errors

Agent API errors include a machine-readable `code` and an `error` message. Use `code` in automation rather than matching message text. Relevant codes include `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `VALIDATION`, `STALE_VERSION`, `CONFLICT`, `DUPLICATE_ADDRESS`, `OVERVIEW_FULL`, `SPACE_FULL`, `BOOKMARK_FULL`, `CROSS_TYPE_MOVE`, `IDEMPOTENCY_CONFLICT`, `RATE_LIMITED`, and `HTTP_ERROR`. A missing endpoint or unsupported HTTP method is an error; do not count its `404` or `405` as a successful read. `OPTIONS` reports the methods available at a valid Agent path in its `Allow` header. A request-shape error uses HTTP `422`; a stale version or capacity denial uses HTTP `409`.

On `409 STALE_VERSION`, fresh-read and verify the intended target before at most one safe retry. For indexed Sheet writes, deletion or compaction may have moved the row: re-identify it by its current cells and index, never just replace `expected_revision` while reusing an old index. Stop and ask the owner if ambiguous. PUT duplicate-match and full-Sheet failures both use `409 CONFLICT` without separate machine-readable subcodes; read current rows/capacity and stop instead of blindly retrying. The [Sheet guide](https://satoshis.watch/api/watch/agent/v1/docs/sheets) explains these cases. Other `409` codes require their own recovery and are not automatically retryable.

## Rate limits

Regular `/api/watch/` requests use an nginx budget of 30 requests per minute per client IP, with a burst allowance of 20. Sheet routes have a separate 20 requests per minute per client IP budget and a global 20 requests per second ceiling; they do not consume the regular account budget. Agent authentication also limits requests to 240 per minute per client IP and per key. Sheet writes have a separate account limit of 30 per minute; accepted PATCH/PUT requests count even when content is unchanged, while already-compact POST does not consume a write. A `429` means to slow down; honor `Retry-After` when present and use backoff before retrying. An Agent API edge `429` includes `code: "RATE_LIMITED"` and links to these Agent docs. Other Wallet+ requests from the same IP may consume the regular allowance.

`/me.rate_limit.remaining` is `null` because the shared nginx allowance is not exposed per key; the `429` response and `Retry-After` header are the source of truth.
