Quicknode's Key-Value Store product can be accessed directly from your Streams filters using the qnLib methods (JavaScript) or the qn SDK (Go). This enables powerful use cases like maintaining watchlists, storing contract ABIs, and building stateful filters that remember data across blocks.
Why Use Key-Value Store in Filters?
Without Key-Value Store, filters are stateless—they process each block independently with no memory of previous blocks. Key-Value Store changes this by giving your filters access to persistent storage:
- Watchlists — Maintain lists of addresses to monitor and check transactions against them
- Contract ABIs — Store ABIs externally so you can update them without modifying filter code
- State tracking — Remember values across blocks (e.g., track cumulative totals, flag seen transactions)
- Dynamic configuration — Change filter behavior by updating stored values, no redeployment needed
Go filters have full access to the Key-Value Store via the qn SDK — reads: qn.ContainsListItem, qn.ContainsListItems, qn.GetValue, qn.GetList, qn.GetAllKeys, qn.GetAllLists; writes: qn.AddListItem, qn.RemoveListItem, qn.DeleteList, qn.AddValue, qn.DeleteValue. Go calls are synchronous (no await needed), and write methods return a Go error directly — always check it.
All qnLib methods are asynchronous and return Promises. Always use the await keyword when calling these methods, and ensure your filter's main function is declared with async.
Data Types: Lists vs Values
Key-Value Store offers two data structures, each suited to different use cases:
| Type | What it stores | Best for |
|---|---|---|
| Lists | Collections of items (e.g., addresses) | Watchlists, allowlists, blocklists |
| Values | Key-value pairs | ABIs, configuration, counters |
Working with Lists
Lists are ideal for maintaining collections of items you want to check against streaming data—like a watchlist of wallet addresses.
Available Methods
- Go
- JavaScript
Go filters access lists through the qn SDK. All calls are synchronous, and the write methods return a Go error you should check:
| Method | Description |
|---|---|
qn.AddListItem(listName, item) | Add single item to a list. Returns error. |
qn.RemoveListItem(listName, item) | Remove single item from a list. Returns error. |
qn.ContainsListItem(listName, item) | Check a single item for membership. Returns bool. |
qn.ContainsListItems(listName, items) | Batch check many items at once. Returns []bool index-aligned with items. Always batch lookups to reduce round trips. |
qn.GetList(listName) | Returns every item in a list. Returns ([]string, error). |
qn.GetAllLists() | Returns every list name for the account. Returns ([]string, error). |
qn.DeleteList(listName) | Deletes a list. Returns error. |
| Method | Description |
|---|---|
qnUpsertList(listId, {add_items, remove_items}) | Create or update a list |
qnAddListItem(listId, item) | Add single item to a list |
qnRemoveListItem(listId, item) | Remove single item from a list |
qnContainsListItems(listId, items[]) | Batch check if items exist in list (returns boolean[]). Always batch lookups to reduce round trips. |
qnDeleteList(listId) | Delete an entire list |
Example: List Operations
This example demonstrates all list operations:
- Go
- JavaScript
type Payload struct {
Data []interface{} `json:"data"`
Metadata map[string]interface{} `json:"metadata"`
}
func Filter(qn *qn.QNContext, payload Payload) interface{} {
const listName = "list_docs_example"
// Results for each operation
results := map[string]interface{}{}
// Add items to the list
if err := qn.AddListItem(listName, "item1"); err != nil {
results["addItem1Error"] = err.Error()
}
if err := qn.AddListItem(listName, "item2"); err != nil {
results["addItem2Error"] = err.Error()
}
// Remove an item from the list
if err := qn.RemoveListItem(listName, "item1"); err != nil {
results["removeItemError"] = err.Error()
}
// Read every item in the list
if items, err := qn.GetList(listName); err != nil {
results["getListError"] = err.Error()
} else {
results["items"] = items
}
// Batch check multiple items for list membership (one round trip)
results["contains"] = qn.ContainsListItems(listName, []string{"item1", "item2"})
// List every list name for the account
if lists, err := qn.GetAllLists(); err != nil {
results["getAllListsError"] = err.Error()
} else {
results["lists"] = lists
}
// Delete the list
if err := qn.DeleteList(listName); err != nil {
results["deleteListError"] = err.Error()
}
return results
}
async function main() {
// List of results for each operation
let results = {}
try {
// Create a new list
results.createList = await qnLib.qnUpsertList('list_docs_example', {
add_items: ['item1', 'item2'],
})
// Update a list
results.qnUpsertList = await qnLib.qnUpsertList('list_docs_example', {
add_items: ['item3'],
remove_items: ['item1'],
})
// Add item to the list
results.qnAddListItem4 = await qnLib.qnAddListItem('list_docs_example', 'item4')
results.qnAddListItem5 = await qnLib.qnAddListItem('list_docs_example', 'item5')
// Remove item from the list
results.qnRemoveListItem4 = await qnLib.qnRemoveListItem('list_docs_example', 'item4')
// Batch check multiple items for list membership (one round trip)
results.qnContainsListItems = await qnLib.qnContainsListItems(
'list_docs_example',
['item1', 'item2']
)
// Delete the list
results.qnDeleteList = await qnLib.qnDeleteList('list_docs_example')
} catch (error) {
results.error = error.message
}
return results
}
Working with Values
Values store key-value pairs—perfect for configuration values, contract ABIs, or any data you want to retrieve by key.
Available Methods
- Go
- JavaScript
In Go, values are accessed through the qn SDK. All calls are synchronous, and write methods return a Go error you should check:
| Method | Description |
|---|---|
qn.AddValue(key, value) | Creates or updates a key-value pair. Returns error. |
qn.GetValue(key) | Retrieves the value for a key. Returns (string, error). |
qn.GetAllKeys() | Lists every key for the account. Returns ([]string, error). |
qn.DeleteValue(key) | Deletes a key-value pair. Returns error. |
| Method | Description |
|---|---|
qnAddValue(key, value) | Create or update a key-value pair |
qnGetValue(key) | Retrieve value by key |
qnDeleteValue(key) | Delete a key-value pair |
qnBulkValues({add_sets, delete_sets}) | Batch create/delete multiple values |
qnListAllKeys() | List all keys |
Note: Always use the
Valuemethods above. The olderSet-named methods are deprecated aliases — prefer theValueequivalents.
Example: Value Operations
This example demonstrates all value operations:
- Go
- JavaScript
type Payload struct {
Data []interface{} `json:"data"`
Metadata map[string]interface{} `json:"metadata"`
}
func Filter(qn *qn.QNContext, payload Payload) interface{} {
// Results for each operation
results := map[string]interface{}{}
// Create (or update) a key-value pair
if err := qn.AddValue("set_docs_example", "value1"); err != nil {
results["addValueError"] = err.Error()
}
// Read a value by key
if value, err := qn.GetValue("set_docs_example"); err != nil {
results["getValueError"] = err.Error()
} else {
results["value"] = value
}
// Bulk add — no batch method in Go, so loop
for key, value := range map[string]string{
"set_docs_example2": "value1",
"set_docs_example3": "value2",
} {
if err := qn.AddValue(key, value); err != nil {
results["addValueError_"+key] = err.Error()
}
}
// List all keys for the account
if keys, err := qn.GetAllKeys(); err != nil {
results["getAllKeysError"] = err.Error()
} else {
results["keys"] = keys
}
// Delete key-value pairs
if err := qn.DeleteValue("set_docs_example2"); err != nil {
results["deleteValueError2"] = err.Error()
}
if err := qn.DeleteValue("set_docs_example3"); err != nil {
results["deleteValueError3"] = err.Error()
}
return results
}
async function main() {
// List of results for each operation
let results = {}
try {
// Create (or update) a value
results.qnAddValue = await qnLib.qnAddValue('value_docs_example', 'value1')
// Get a value
results.qnGetValue = await qnLib.qnGetValue('value_docs_example')
// Get all keys
results.qnListAllKeys = await qnLib.qnListAllKeys()
// Bulk add/remove values
results.qnBulkValues = await qnLib.qnBulkValues({
add_sets: {
value_docs_example2: 'value1',
value_docs_example3: 'value2',
},
delete_sets: ['value_docs_example1'],
})
// Get all keys after bulk
results.qnListAllKeys2 = await qnLib.qnListAllKeys()
// Delete values
results.qnDeleteValue2 = await qnLib.qnDeleteValue('value_docs_example2')
results.qnDeleteValue3 = await qnLib.qnDeleteValue('value_docs_example3')
} catch (error) {
results.error = error.message
}
return results
}
Managing ABIs with Key-Value Store
You can store and manage your contract ABIs using Key-Value Store:
- Upload ABIs using REST APIs
- Update ABIs when contracts are upgraded
- Load multiple ABIs dynamically in your filter
- Share ABIs across different streams
Common Pitfalls
The pitfalls below are specific to JavaScript's Promise-based qnLib API. Go's qn SDK is synchronous — there are no Promises to await, so these issues don't apply to Go filters. In Go, the equivalent discipline is to check the error returned by every write method (qn.AddListItem, qn.RemoveListItem, qn.DeleteList, qn.AddValue, qn.DeleteValue) and to batch membership checks with qn.ContainsListItems rather than calling qn.ContainsListItem in a loop.
Missing await Keyword
All qnLib methods are asynchronous and return Promises. Failing to use await will result in unexpected behavior:
// INCORRECT - returns a Promise, not the actual result
function main() {
const result = qnLib.qnContainsListItems('myList', ['item']);
// result is a Promise object, not the boolean array
return result;
}
// CORRECT - returns the resolved value from the Promise
async function main() {
const result = await qnLib.qnContainsListItems('myList', ['item']);
// result contains the index-aligned boolean array
return result;
}
Boolean Checks with Async Methods
When checking list membership, use qnContainsListItems and batch your lookups. Be careful to await the result when using it in conditionals:
// INCORRECT - checks if Promise exists, not the result
function main() {
if (qnLib.qnContainsListItems('myList', ['item'])) {
// This will always evaluate to true regardless of whether
// the item is in the list or not!
return { exists: true };
}
return { exists: false };
}
// CORRECT - batch lookup, then use the index-aligned boolean array
async function main() {
const hits = await qnLib.qnContainsListItems('myList', ['item']);
if (hits[0]) {
return { exists: true };
}
return { exists: false };
}
Error Handling for Large Operations
When working with large datasets, implement proper error handling:
// INCORRECT - no error handling for large list operations
async function main() {
// This might fail with large lists
await qnLib.qnUpsertList('myList', { add_items: veryLargeArray });
return { success: true };
}
// CORRECT - handles potential size-related errors
async function main() {
try {
// Use chunking for large operations
const chunkSize = 50000;
for (let i = 0; i < veryLargeArray.length; i += chunkSize) {
const chunk = veryLargeArray.slice(i, i + chunkSize);
await qnLib.qnUpsertList('myList', { add_items: chunk });
}
return { success: true };
} catch (error) {
return { error: `Failed to update list: ${error.message}` };
}
}
Cascading Async Operations
Chain operations carefully to avoid cascading failures:
// INCORRECT - continues execution even if a prerequisite operation fails
async function main() {
await qnLib.qnAddListItem('myList', 'important_item');
const hits = await qnLib.qnContainsListItems('myList', ['important_item']);
return { found: hits[0] };
}
// CORRECT - checks intermediate results before continuing
async function main() {
try {
const addResult = await qnLib.qnAddListItem('myList', 'important_item');
if (!addResult) {
return { error: "Failed to add item to list" };
}
const hits = await qnLib.qnContainsListItems('myList', ['important_item']);
return { found: hits[0] };
} catch (error) {
return { error: `Operation failed: ${error.message}` };
}
}
Performance: Batch List Lookups
Batch all list membership checks into as few calls as possible to reduce round trips to the Key-Value Store. This batch lookup is optimized for performance.
- JavaScript:
qnLib.qnContainsListItems(listId, items[])→Promise<boolean[]>(index-aligned toitems) - Go:
qn.ContainsListItems(listId, items []string) []bool(index-aligned toitems, synchronous)
- Go
- JavaScript
// Gather all addresses, then one batched lookup — minimizes round trips.
type Payload struct {
Data []Block `json:"data"`
Metadata map[string]interface{} `json:"metadata"`
}
type Block struct {
Transactions []Transaction `json:"transactions"`
}
type Transaction struct {
From string `json:"from"`
To string `json:"to"`
}
func Filter(qn *qn.QNContext, payload Payload) interface{} {
const listID = "WATCHLIST_EVM_MAINNET"
// Gather from/to, de-duped, before the single batch call
seen := make(map[string]bool)
var addrs []string
for _, block := range payload.Data {
for _, tx := range block.Transactions {
if tx.From != "" {
from := strings.ToLower(tx.From)
if !seen[from] {
seen[from] = true
addrs = append(addrs, from)
}
}
if tx.To != "" {
to := strings.ToLower(tx.To)
if !seen[to] {
seen[to] = true
addrs = append(addrs, to)
}
}
}
}
if len(addrs) == 0 {
return nil
}
hits := qn.ContainsListItems(listID, addrs)
// Fast lookup map (index-aligned to addrs)
hitMap := make(map[string]bool, len(addrs))
for i, addr := range addrs {
hitMap[addr] = hits[i]
}
// Return matched txs
var matched []Transaction
for _, block := range payload.Data {
for _, tx := range block.Transactions {
if hitMap[strings.ToLower(tx.From)] || hitMap[strings.ToLower(tx.To)] {
matched = append(matched, tx)
}
}
}
if len(matched) == 0 {
return nil
}
return map[string]interface{}{"data": matched}
}
// Gather all addresses, then one batched lookup — minimizes round trips.
async function main(stream) {
const listId = "WATCHLIST_EVM_MAINNET";
// Gather from/to
const addrs = [];
for (const chunk of stream.data ?? []) {
for (const tx of chunk.transactions ?? []) {
if (tx.from) addrs.push(tx.from.toLowerCase());
if (tx.to) addrs.push(tx.to.toLowerCase());
}
}
if (addrs.length === 0) return null;
// De-dup before the single batch call
const uniq = Array.from(new Set(addrs));
const hits = await qnLib.qnContainsListItems(listId, uniq);
// Fast lookup map (index-aligned to uniq)
const hitMap = new Map(uniq.map((a, i) => [a, !!hits[i]]));
// Return matched txs
const matched = [];
for (const chunk of stream.data ?? []) {
for (const tx of chunk.transactions ?? []) {
const f = tx.from?.toLowerCase();
const t = tx.to?.toLowerCase();
if ((f && hitMap.get(f)) || (t && hitMap.get(t))) matched.push(tx);
}
}
return matched.length ? { data: matched } : null;
}
When you need to check list membership for multiple values (e.g. many tx.from/tx.to or many log addresses), collect them, de-duplicate, then call qnContainsListItems (JavaScript) or ContainsListItems (Go) once and use the returned boolean array. Avoid multiple per-item lookups.