From 6d1375ab583fd0eadb42759b84f533a509bdf25a Mon Sep 17 00:00:00 2001 From: Sergen Uysal <0xsergen@gmail.com> Date: Fri, 4 Sep 2026 18:08:44 +0300 Subject: [PATCH 1/3] [CON-902] Correct Key-Value Store reference against the live product Audited kv-reference.md against qn 0.6.0, the kv/rest/v1 REST API, and qnLib inside `qn stream test-filter`. 10 confirmed defects. Every claim in the file was executed. Silent failures. Each of these returns a 2xx and gives nothing usable: - `GET /sets` destructured `{ keys }`. The envelope is `{ code, msg, data, cursor }`. There is no `keys` field. - `GET /sets/{key}` destructured `{ value }`. The value is at `data.value`. - `contains` destructured `{ contains }`. The API returns `data.exists`. Every membership check reported "not a member", including for real members. An allowlist built on this example fails silently and permanently. This was the worst of the ten. - `PATCH /lists/{key}` ignores snake_case fields and still returns 200 "List updated successfully". Nothing is written. - qnLib is the mirror image: it needs snake_case, and camelCase is the silent no-op. Documented the casing rule on both layers. - Every qnLib write helper returns the string "OK", including on a no-op. "OK" is truthy, so it is not a success signal. Documented. Wrong semantics: - "Create or overwrite a list" is false. `qnUpsertList` merges. An agent resetting a watchlist accumulates instead. Documented `remove_items`, which was also missing. - "Ordered lists" is false. Lists sort lexicographically at both the REST and qnLib layers. Limits table: all three rows were wrong. Boundary-probed to the exact value. - Max key length: 255 characters, not 256 bytes. - Max value size: 800,000 characters, not 64 KB. The old figure was about 12x too small, so agents chunked data needlessly. - Max list size: the 10,000-item cap does not exist. A list grew to 10,503 with no error. The real cap is 1,500 items per write, and it is the one that returns 400. Coverage: - 21 qnLib helpers are exposed and 9 were documented. Added `qnGetList`, `qnGetAllLists`, `qnContainsListItem`, the `...Value` aliases, and `getAccountId` / `signPayload` / `validatePayload`. `qnGetList` is the important one: the file gave no way to read a list from inside a filter. - Added the four missing CLI commands: `kv set bulk`, `kv list ls`, `kv list remove-item`, `kv list update`. Verification: 38/38 assertions pass (12 REST, 18 qnLib, 8 CLI). All test resources used the `con901-` prefix and were deleted in session. Set and list counts returned to 31 and 14. Version bumps for the three plugin manifests land in the CON-905 PR, the last of this set to merge. --- .../references/quicknode/kv-reference.md | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md b/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md index 52fde59..1d9d9e7 100644 --- a/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md +++ b/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md @@ -16,14 +16,17 @@ Quicknode KV Store is a serverless storage service for lists and key-value sets. `qnLib` is available inside Streams filter functions without any import. All calls are asynchronous and should be awaited from an `async function main(...)` filter. +Write helpers return the string `"OK"`. They return `"OK"` whether or not they changed anything, so the return value is not a success signal — read the value back to confirm. Option keys are snake_case (`add_items`, `remove_items`, `add_sets`, `delete_sets`); a camelCase key is ignored and the call still returns `"OK"`. + ### List Operations -Manage ordered lists of string items (e.g., a watchlist of wallet addresses). +Manage lists of string items (e.g., a watchlist of wallet addresses). Items are returned sorted lexicographically, not in insertion order. ```javascript -// Create or overwrite a list +// Create a list, or merge items into an existing one await qnLib.qnUpsertList('my-watchlist', { add_items: ['0xAddr1', '0xAddr2'], + remove_items: [], }); // Add a single item @@ -32,14 +35,25 @@ await qnLib.qnAddListItem('my-watchlist', '0xAddr3'); // Remove a single item await qnLib.qnRemoveListItem('my-watchlist', '0xAddr1'); -// Check membership (returns array of booleans, one per address) -const results = await qnLib.qnContainsListItems('my-watchlist', ['0xAddr2', '0xAddr3']); -// results → [true, true] +// Read the list — returns a plain array of strings +const items = await qnLib.qnGetList('my-watchlist'); +// items → ['0xAddr2', '0xAddr3'] + +// All list keys on the account +const listKeys = await qnLib.qnGetAllLists(); + +// Check membership +const one = await qnLib.qnContainsListItem('my-watchlist', '0xAddr2'); +// one → true +const many = await qnLib.qnContainsListItems('my-watchlist', ['0xAddr2', '0xAddr3']); +// many → [true, true] -// Delete the entire list +// Delete the entire list — a subsequent qnGetList returns [] await qnLib.qnDeleteList('my-watchlist'); ``` +`qnUpsertList` merges into an existing list; it does not replace it. Delete the list first to replace its contents. + ### Set Operations Manage key-value pairs (string keys, string values). @@ -61,17 +75,25 @@ await qnLib.qnBulkSets({ delete_sets: [], }); -// List all set keys +// List all set keys — array of key strings const setKeys = await qnLib.qnListAllSets(); // Delete a single set entry await qnLib.qnDeleteSet('threshold'); ``` +`qnGetSet` returns `null` for a key that does not exist. `qnAddValue`, `qnGetValue`, `qnDeleteValue`, `qnBulkValues`, and `qnListAllKeys` are aliases for the `…Set`/`…Sets` helpers and behave identically. + +`qnLib` also exposes `getAccountId()`, and `signPayload`/`validatePayload` for webhook signatures. + ## REST API All REST requests use `https://api.quicknode.com/kv/rest/v1/` and authenticate via the `x-api-key` header. +Every response is wrapped in an envelope: `{ "code": 200, "msg": "…", "data": … }`, plus `cursor` on paginated reads. The payload is always under `data`. The envelope `code` is independent of the HTTP status — a successful write returns HTTP `201` with `code: 200`. + +Request bodies use camelCase (`addItems`, `removeItems`). A snake_case key is ignored and the request still returns `200`. + ### Read a value ```typescript @@ -79,7 +101,8 @@ const response = await fetch( 'https://api.quicknode.com/kv/rest/v1/sets/threshold', { headers: { 'x-api-key': process.env.QUICKNODE_API_KEY! } } ); -const { value } = await response.json(); +const { data } = await response.json(); +// data → { key: 'threshold', value: '750000' } ``` ### Write a value @@ -105,7 +128,8 @@ const response = await fetch( 'https://api.quicknode.com/kv/rest/v1/sets', { headers: { 'x-api-key': process.env.QUICKNODE_API_KEY! } } ); -const { keys } = await response.json(); +const { data, cursor } = await response.json(); +// data → [{ key: 'threshold', value: '750000' }, …] ``` ### List operations @@ -124,14 +148,34 @@ await fetch('https://api.quicknode.com/kv/rest/v1/lists', { }), }); +// Read a list +const listResponse = await fetch( + 'https://api.quicknode.com/kv/rest/v1/lists/allowlist', + { headers: { 'x-api-key': process.env.QUICKNODE_API_KEY! } } +); +const { data } = await listResponse.json(); +// data → { items: ['0xabc', '0xdef'] } + +// Add or remove items on an existing list +await fetch('https://api.quicknode.com/kv/rest/v1/lists/allowlist', { + method: 'PATCH', + headers: { + 'x-api-key': process.env.QUICKNODE_API_KEY!, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ addItems: ['0x123'], removeItems: ['0xabc'] }), +}); + // Check one item from outside a Stream filter const response = await fetch( 'https://api.quicknode.com/kv/rest/v1/lists/allowlist/contains/0xabc', { headers: { 'x-api-key': process.env.QUICKNODE_API_KEY! } } ); -const { contains } = await response.json(); +const { data: { exists } } = await response.json(); ``` +`GET /lists` returns `{ data: { keys: [...] }, cursor }` — note `data` is an object here, while `GET /sets` returns `data` as an array. + ## CLI — `qn kv` ```bash @@ -147,11 +191,17 @@ qn kv set ls # Delete a key qn kv set delete threshold +# Add and/or delete several keys in one call +qn kv set bulk --add threshold=750000 --delete old_threshold + # Manage lists +qn kv list ls qn kv list create allowlist 0xabc 0xdef qn kv list append allowlist 0x123 qn kv list contains allowlist 0xabc qn kv list get allowlist +qn kv list remove-item allowlist 0xabc +qn kv list update allowlist --add 0x456 --remove 0xdef qn kv list delete allowlist ``` @@ -159,9 +209,11 @@ qn kv list delete allowlist | Limit | Value | |-------|-------| -| Max value size | 64 KB | -| Max list size | 10,000 items | -| Max key length | 256 bytes | +| Max key length | 255 characters | +| Max value length | 800,000 characters | +| Max items per list write | 1,500 (`addItems` + `removeItems` combined) | + +Exceeding a limit returns HTTP `400` with the limit named in `message`. Lists longer than 1,500 items are built with repeated writes; no total list length is enforced. Limits are subject to change — check https://www.quicknode.com/docs/key-value-store for current values. From b5c5d813c84e78c8bbbbbf41e7f6e75be54933e5 Mon Sep 17 00:00:00 2001 From: Sergen Uysal <0xsergen@gmail.com> Date: Fri, 4 Sep 2026 18:25:27 +0300 Subject: [PATCH 2/3] [CON-902] Add Test: lines for the re-verified KV examples CON-901 asks for a Test: line on every example. Added five, each asserting the response envelope, which is what the three worst defects in this file got wrong. Envelope shape is the right thing to assert here: it is identical for every reader, while key counts are not. Re-verified live in this session: - GET /sets/{key}: data is { key, value }, no top-level value. - POST /sets: HTTP 201 with { code: 200, msg: 'Key value stored', data: null }. This confirms the envelope code is independent of the HTTP status. - GET /sets: top-level code, msg, data, cursor, and data is an array. No keys field. - GET /lists/{key}/contains/{item}: data is { exists: true } for a member and { exists: false } for a non-member. No contains field. - Limits, boundary-probed again to the exact value: key 255 gives 201 and 256 gives 400; value 800,000 gives 201 and 800,001 gives 400; a 1,500-item write gives 200 and 1,501 gives 400 with "total of addItems and removeItems is 1501, max allowed is 1500". Also re-confirmed, unchanged: GET /lists returns data as an object holding keys, unlike GET /sets; GET /lists/{key} returns data as { items }; a snake_case add_items returns 200 and writes nothing, while addItems writes and merges rather than replaces; and a list seeded zebra, mango, apple reads back apple, mango, zebra. The qnLib and CLI examples carry no Test: line. Both operate on the reader's own store, so the only assertable value is this account's key count, which no other reader can compare against. All probe resources used the con901-audit-kv-20260904 prefix and were deleted in session. Sets and lists returned to 31 and 14, with zero con901 leftovers. Version bumps for the three plugin manifests land in the CON-905 PR, the last of this set to merge. --- .../build-web3/references/quicknode/kv-reference.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md b/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md index 1d9d9e7..7c5be1c 100644 --- a/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md +++ b/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md @@ -105,6 +105,8 @@ const { data } = await response.json(); // data → { key: 'threshold', value: '750000' } ``` +**Test:** `GET /sets/{key}` — `data` is `{ key, value }`. There is no top-level `value`, so destructuring `{ value }` yields `undefined` + ### Write a value ```typescript @@ -121,6 +123,8 @@ const response = await fetch('https://api.quicknode.com/kv/rest/v1/sets', { }); ``` +**Test:** `POST /sets` — HTTP `201` with body `{ code: 200, msg: 'Key value stored', data: null }` + ### List all set keys ```typescript @@ -132,6 +136,8 @@ const { data, cursor } = await response.json(); // data → [{ key: 'threshold', value: '750000' }, …] ``` +**Test:** `GET /sets` — top-level keys are `code`, `msg`, `data`, `cursor`, and `data` is an array. There is no `keys` field + ### List operations ```typescript @@ -174,6 +180,8 @@ const response = await fetch( const { data: { exists } } = await response.json(); ``` +**Test:** `GET /lists/{key}/contains/{item}` — `data` is `{ exists: true }` for a member and `{ exists: false }` for a non-member. There is no `contains` field + `GET /lists` returns `{ data: { keys: [...] }, cursor }` — note `data` is an object here, while `GET /sets` returns `data` as an array. ## CLI — `qn kv` @@ -213,6 +221,8 @@ qn kv list delete allowlist | Max value length | 800,000 characters | | Max items per list write | 1,500 (`addItems` + `removeItems` combined) | +**Test:** boundary-probed — a 255-character key returns `201` and 256 returns `400`; an 800,000-character value returns `201` and 800,001 returns `400`; a 1,500-item write returns `200` and 1,501 returns `400` with `total of addItems and removeItems is 1501, max allowed is 1500` + Exceeding a limit returns HTTP `400` with the limit named in `message`. Lists longer than 1,500 items are built with repeated writes; no total list length is enforced. Limits are subject to change — check https://www.quicknode.com/docs/key-value-store for current values. From 0f7d38f2dcb8b552bf5d5d51a89537883e2111f1 Mon Sep 17 00:00:00 2001 From: Sergen Uysal <0xsergen@gmail.com> Date: Fri, 4 Sep 2026 18:45:08 +0300 Subject: [PATCH 3/3] [CON-902] Document the CLI JSON shapes and the --yes requirement Two gaps found on the 2026-09-04 re-verification pass. Both are the same defect class as D1 through D3: a reader destructures a field that is not there, and gets undefined instead of an error. The REST envelope is documented above the CLI section, so a reader reasonably carries it downward. It does not hold. With `-o json` the CLI uses no single envelope, and two of the five reads put the payload at the top level with no `data` wrapper at all: qn kv set list {data: [{key, value}], cursor} qn kv set get {value} qn kv list ls {data: {keys: [...]}, cursor} qn kv list get {data: {items: [...]}, cursor} qn kv list contains {exists} Note `keys` for sets and `items` for lists. Note also that `.data.exists` on `qn kv list contains` is undefined, which is falsy, so a membership check written that way always reports "not a member". That is exactly the D3 failure, reached through the CLI instead of REST. Second gap: every delete command needs `--yes` when no terminal is attached. Without it the command exits non-zero with "operation requires confirmation" and deletes nothing. An agent scripting its own cleanup fails silently against that. Found by hitting it during this audit's own resource cleanup. Also recorded: `qn kv set get` on a missing key exits non-zero with "Error: not found." It does not return an empty value. Verified live against 31 sets and 14 lists. The probe key con901-audit-kv-20260904-shape was created and deleted in the same session. Final sweep: 31 sets, 14 lists, zero con901 leftovers. --- .../references/quicknode/kv-reference.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md b/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md index 7c5be1c..1e7951c 100644 --- a/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md +++ b/plugins/build-web3/skills/build-web3/references/quicknode/kv-reference.md @@ -213,6 +213,29 @@ qn kv list update allowlist --add 0x456 --remove 0xdef qn kv list delete allowlist ``` +Every delete command needs `--yes` when no terminal is attached. Without it the +command exits non-zero with `operation requires confirmation; pass --yes to +proceed without an interactive prompt`, and nothing is deleted. + +With `-o json`, the CLI does not use one envelope. Two of these five reads put +the payload at the top level, with no `data` wrapper: + +| Command | Shape | +|---------|-------| +| `qn kv set list` | `{ data: [{ key, value }], cursor }` | +| `qn kv set get` | `{ value }` | +| `qn kv list ls` | `{ data: { keys: [...] }, cursor }` | +| `qn kv list get` | `{ data: { items: [...] }, cursor }` | +| `qn kv list contains` | `{ exists }` | + +Read the field this table names for the command you ran. The REST envelope in +the section above does not apply to CLI output. + +`qn kv set get` on a key that does not exist prints `Error: not found.` and +exits non-zero. It does not return an empty value. + +**Test:** `qn kv set list` — 31 sets, each `{key, value}`; `qn kv list ls` — 14 lists under `data.keys`; `qn kv list contains ` — `{"exists": false}` at the top level + ## Limits | Limit | Value |