# KVStore Lookup Service (`ls_kvstore`)

Answers `POST /lookup` with `question.service = "ls_kvstore"` over the records
`tm_kvstore` admitted.

## Query

Every field is optional. `"findAll"` (string) and `{}` both mean "the newest
page of everything". Unknown fields are refused with
`ERR_INVALID_LOOKUP_QUERY` (400) rather than silently ignored.

```json
{
  "protocolID": [0, "Microblog Posts v1"],
  "controller": "02…66 hex…",
  "key": "post:abc",
  "tags": ["post_id_abc"],
  "tagQueryMode": "all",
  "limit": 20,
  "cursor": "<txid>.<outputIndex>",
  "sortOrder": "desc",
  "history": false
}
```

| field | meaning |
|---|---|
| `protocolID` | wallet protocol, as `[level, "name"]` or its JSON text |
| `controller` | exact controller identity key (66 hex, either case) |
| `key` | exact wallet key id |
| `tags` | tags to match |
| `tagQueryMode` | `all` (every tag, the default) or `any` (at least one) |
| `limit` | page size, default 50, clamped to 200; `0` is refused |
| `skip` | rows to skip; refused together with `cursor` |
| `sortOrder` | `desc` (newest admission first, default) or `asc` |
| `cursor` | the last row of the previous page, `"<txid>.<outputIndex>"` |
| `history` | refused with its reason (see below) |

## Current values only

An update spends the previous token, so **unspent is current**: a query never
returns a spent outpoint. If two unspent rows exist for one key (a fork, or a
client that has not yet spent the old token), BOTH are served: the chain is the
source of truth and this service does not adjudicate.

Previous versions are **not** served. `history: true` is refused with that
reason, because the engine builds an answer from the outputs it still holds and
a spend removes that row: the index keeps the row and its `spent` mark (which is
what keeps current reads correct), but the BEEF of a spent output is no longer
available here. Read a key's versions from the BEEF ancestry you already hold —
the SDK's `Historian` does exactly that.

## Pagination

`skip` walks by position, so a row inserted between two requests shifts every
later page — the defect Microblog documents in its feed pagination notes.

`cursor` walks by admission sequence instead. Pass the txid and output index of
the **last row you received** (`outputIndex` is on the item; the txid is the
BEEF's subject). The answer is strictly after that row, so rows admitted between
pages cannot displace, repeat or skip an older one.

Rows are never deleted, only flagged spent or retired, so a cursor stays
resolvable across updates. A cursor naming an outpoint this service never
indexed is refused (400) rather than silently restarting from page one.

## Answer

`type: "output-list"`, one item per matching current value (or per version with
`history: true`), in the requested order:

```json
{ "type": "output-list",
  "outputs": [ { "beef": [ … ], "outputIndex": 0 } ] }
```

`beef` is the transaction and its ancestry; the client decodes the KVStore
fields (protocol, key, value, controller, tags) from the locking script and
verifies the signature itself.

## Example

```js
const answer = await overlayClient.lookup({
  service: 'ls_kvstore',
  query: { protocolID: [0, 'Microblog Posts v1'], limit: 20, sortOrder: 'desc' }
})
const next = answer.outputs.length === 20
  ? `${txidOf(answer.outputs.at(-1))}.${answer.outputs.at(-1).outputIndex}`
  : null
```
