This example manages a wallet watchlist and a minimum native ETH value in Key-Value Store, then creates a Stream whose filter reads both. Use it to alert on native ETH transfers involving watched wallets: matching Ethereum transactions arrive at your webhook. Change the watchlist or threshold with Terraform or OpenTofu; the filter reads the current KV data without a filter redeploy.
You need an Admin API key, a plan that supports Streams, and a webhook that can accept Quicknode deliveries. Streams processing and KV storage can consume API credits. Use a disposable test destination and review the plan before applying.
If you need a key, sign in to the Quicknode Dashboard, open the account menu at the bottom left, and select API Keys. Click Add API Key, choose the Admin role, and enable Admin API, Streams, and Key-Value Store access before creating the key.
Configure the provider
Save the files shown below together in one empty directory. See the provider references for KV list items, KV values, and Streams for their available arguments. Set a webhook URL you control; do not use the placeholder address in a live Stream.
provider.tf:
terraform {
required_providers {
quicknode = {
source = "quicknode/quicknode"
version = "~> 0.4.0"
}
}
}
provider "quicknode" {}
variable "list_key" {
description = "Account-scoped KV list used by the example Stream."
type = string
default = "iac-docs-wallet-watch"
}
variable "threshold_key" {
description = "Account-scoped KV value used by the example Stream."
type = string
default = "iac-docs-min-wei"
}
Manage the watchlist and threshold
kv.tf creates two managed list items and a threshold in wei. Replace the sample addresses with lowercase addresses you actually want to watch. The default threshold of 0 matches any transaction value; raise it to limit deliveries. The filter checks the transaction's native ETH value, not ERC-20 token transfer amounts.
resource "quicknode_kv_list_items" "watchlist" {
list_key = var.list_key
items = [
"0x1111111111111111111111111111111111111111",
"0x2222222222222222222222222222222222222222",
]
}
resource "quicknode_kv_value" "minimum_wei" {
key = var.threshold_key
value = "0"
}
quicknode_kv_list_items owns only these two entries, so another writer can add items to the list. Creating the resource fails if the configured entries already exist; import them if Terraform should take ownership. The KV value is different: if another process changes it, the next apply restores the configured threshold.
Deploy the Stream and filter
stream.tf uses a webhook destination and waits for both KV resources before creating the Stream. The provider reads the filter source from a separate template file.
variable "webhook_url" {
description = "A test webhook that accepts Quicknode Stream deliveries."
type = string
}
resource "quicknode_stream" "wallet_watch" {
name = "iac-docs-wallet-watch"
network = "ethereum-mainnet"
dataset = "transactions"
region = "usa_east"
status = "active"
filter_function = templatefile("${path.module}/filter.js.tftpl", {
list_key = var.list_key
threshold_key = var.threshold_key
})
destination = {
webhook = {
url = var.webhook_url
}
}
depends_on = [quicknode_kv_list_items.watchlist, quicknode_kv_value.minimum_wei]
}
filter.js.tftpl reads the current threshold and checks transaction addresses against the watchlist in one batched KV lookup. It returns null when nothing matches, so there is no webhook delivery for that batch.
The EVM Transactions dataset delivers an array of transaction arrays, one inner array per block. The filter flattens the batch before checking addresses.
async function main(stream) {
const listKey = ${jsonencode(list_key)}
const thresholdKey = ${jsonencode(threshold_key)}
const threshold = BigInt(await qnLib.qnGetValue(thresholdKey))
const transactions = (stream.data ?? []).flat()
const addresses = [...new Set(transactions.flatMap(tx =>
[tx.from, tx.to].filter(Boolean).map(address => address.toLowerCase())
))]
if (addresses.length === 0) return null
const hits = await qnLib.qnContainsListItems(listKey, addresses)
const watched = new Set(addresses.filter((_, index) => hits[index]))
const matches = transactions.filter(tx =>
(watched.has(tx.from?.toLowerCase()) || watched.has(tx.to?.toLowerCase())) &&
BigInt(tx.value ?? '0x0') >= threshold
)
return matches.length ? { matches } : null
}
Set credentials and the webhook URL, then run the same commands with terraform or tofu:
export QUICKNODE_API_KEY="your-api-key"
export TF_VAR_webhook_url="https://your-test-webhook.example/quicknode"
terraform init
terraform fmt -check
terraform validate
terraform plan
terraform apply
The Stream starts at the latest block unless you set start_range; only future matching transactions will reach the webhook. For a controlled test, use a watchlisted address that will send a transaction, or set a backfill range after checking the Stream resource behavior. Stream filters are tested against their start range during creation, so a missing KV key or invalid filter prevents the Stream from being created.
When finished, review terraform plan -destroy, then run terraform destroy (or the equivalent tofu commands). The Stream, configured KV value, and Terraform-managed watchlist entries are removed. The list key can remain, and items added outside Terraform remain. If you no longer need the list, delete it through the KV REST API after confirming no other writer uses it.