# Sheet Spaces for Agents

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

The account owner must grant an existing Sheet Space `read` or `write` access in API access. A key's own capability is an additional ceiling. A write-capable key can create a new Sheet Space with `POST /bookmark-spaces` and `type: "sheet"` when Watch+ has room and an existing Space is writable. See the [Agent API guide](https://satoshis.watch/api/watch/agent/v1/docs) for Space creation and key setup.

## Read a Sheet

`GET /bookmark-spaces/<SPACE_ID>/sheet?offset=0&limit=100` returns `space_id`, `headers`, `revision`, `row_count: 1000`, `column_count: 5`, `rows`, and `next_offset`. `offset` is an inclusive row-slot index from `0` through `999`, not a count of matches. `limit` defaults to `100` and must be from `1` through `100`. In dense view, it controls how many slots are returned, including blank rows; `next_offset` is the next slot after that range, or `null` at the end.

Use `view=sparse` for nonempty rows only. Sparse/filter views still validate `limit` but ignore its value: each page contains at most 200 matching rows and 256 KiB of row data. Rows are ordered by their current slot indexes, and `next_offset` is the first matching row not included in that page. Continue with that exact value until it is `null`; do not derive offsets from the page length. Exact, case-sensitive `address=<text>` and `chain=<text>` filters match the first two cells and imply sparse view. Keep the same view and filters on subsequent pages. An unsupported `filter=` returns `400`.

For example, `GET /bookmark-spaces/<SPACE_ID>/sheet?view=sparse&offset=0&limit=1&chain=Bitcoin` can return both rows below because sparse page size does not use `limit`:

```json
{
  "space_id": "00000000-0000-4000-8000-000000000001",
  "headers": ["Address", "Blockchain", "Transaction ID", "Date", "Notes"],
  "revision": 4,
  "row_count": 1000,
  "column_count": 5,
  "rows": [
    {"index": 7, "cells": ["address-one", "Bitcoin", "", "", "note"]},
    {"index": 42, "cells": ["address-two", "Bitcoin", "", "", ""]}
  ],
  "next_offset": null
}
```

Each page reads current state; the API has no snapshot cursor or expected-revision parameter for GET. Compare every page's `revision` with the first page. If it changes, discard the combined read and restart from offset 0. Repeated changes mean the Sheet is busy: pace reads and stop rather than claiming a complete or empty Sheet. A write must still use a fresh read and can conflict after pagination finishes.

The first four headings are locked: `Address`, `Blockchain`, `Transaction ID`, and `Date`. The fifth starts as `Notes` but an owner or Agent may rename it; an existing Sheet may show a custom heading such as `Numbers`. Always read and preserve the returned `headers` when editing. Cells are plain text, at most 512 characters each; the server does not evaluate formulas or interpret addresses, dates, or amounts.

## Write rows and headings

Every write needs a write-capable key, a current Space `write` grant, and the integer `expected_revision` from a recent Sheet GET. On `409 STALE_VERSION`, fresh-read and re-identify the intended row, verifying its current index and cells against the owner's requested change. Deletion and compaction reindex rows. Never merely refresh `expected_revision` and reuse an old row index. If the target is unambiguous and the requested scope still applies, retry once with its verified current index and revision; otherwise stop and ask the owner. Other `409` responses are not automatically retryable.

`PATCH /bookmark-spaces/<SPACE_ID>/sheet` accepts `headers`, `rows`, or both. Each accepted PATCH increments the revision once, even when the supplied content is unchanged:

```json
{
  "expected_revision": 0,
  "rows": [
    {"index": 0, "cells": ["bc1...", "Bitcoin", "txid", "2026-09-29", "0.01 BTC"]}
  ]
}
```

Send 1–100 unique row indexes from `0` through `999`, with exactly five strings per row. Blank cells clear their content; five blank cells remove that row's stored content without renumbering its slot. To rename the fifth heading, send the complete `headers` array, preserving the first four exactly and changing only the fifth to 1–60 printable characters.

PATCH returns `space_id`, the complete current `headers`, the new `revision`, and `updated_rows` for the submitted indexes. A headers-only write returns an empty `updated_rows` array. A cleared row is returned with five empty strings.

For a targeted Address+Blockchain upsert, `PUT /bookmark-spaces/<SPACE_ID>/sheet` accepts `{"expected_revision":0,"cells":["bc1...","Bitcoin","txid","2026-09-29","note"]}`. Both key cells must be nonempty. It updates the unique exact match or uses the first empty slot. It returns `space_id`, the new `revision`, `index`, `created`, and the saved `cells`; `created` is `false` when updating a match. Each successful PUT increments the revision, even for identical cells. Duplicate matches and a full Sheet both return `409 CONFLICT`, as described below.

`DELETE /bookmark-spaces/<SPACE_ID>/sheet` with `{"expected_revision":0,"index":7}` clears that slot and subtracts one from each later stored row's index; it does not remove other gaps. It can shift later rows even if the selected slot was blank. It returns `space_id`, the new `revision`, `deleted_index`, and `shifted_rows`. If nothing can be deleted or shifted, it returns `404 NOT_FOUND`.

`POST /bookmark-spaces/<SPACE_ID>/sheet` with `{"expected_revision":0}` compacts nonempty rows at the start while preserving order. It returns `space_id`, `revision`, and `moved_rows`. If already compact, the revision stays unchanged and `moved_rows` is `0`. Reload and verify row identity after deletion or compaction before another indexed write.

## Conflicts

Sheet write errors include `code` and `error`. The implemented `409` cases are:

| Condition | Code | Recovery |
| --- | --- | --- |
| Sheet revision changed | `STALE_VERSION` | Fresh-read and verify the intended target; retry once only if unambiguous and still within the requested scope. |
| PUT finds multiple exact Address+Blockchain matches | `CONFLICT` | Read the matching rows and stop. Ask the owner which row is intended; do not automatically delete or merge duplicates. |
| PUT has no matching row and no empty slot | `CONFLICT` | Read capacity and stop. Clearing or deleting rows needs the owner's explicit cleanup instruction. |

The duplicate-match message is `More than one row has this address and chain.`; the full-Sheet message is `Sheet has no empty row slots.`. Both share `CONFLICT`; there are no distinct machine-readable subcodes. Do not branch automation on the message text or retry an undifferentiated conflict. Compaction preserves the number of filled rows and cannot make room in a full Sheet. Grant/type changes can also deny an operation; stop on other errors instead of assuming a revision retry is safe.

## Delete a Sheet Space

To delete the Sheet Space itself, clear every nonempty row first. Follow `GET /sheet?view=sparse` through its final page to check for remaining content; `row_count: 1000` is the fixed slot capacity, not a count of filled rows. Then reload the Space `version` and `bookmark_revision` and send the confirmed `DELETE /bookmark-spaces/<SPACE_ID>` described in the [Agent API guide](https://satoshis.watch/api/watch/agent/v1/docs). A custom fifth heading does not need to be reset. Nonempty rows return `409` without deleting the Space.

The account has a shared limit of 30 Sheet writes per minute; an accepted PATCH or PUT consumes that budget even when content is unchanged. Already-compact POST does not consume a write. Sheet requests have a separate edge traffic limit; honor `429` and `Retry-After`. Avoid storing API keys, Watch Keys, passwords, or seed phrases in cells.
