Key-value stores
Create and manage your own whitelists, blacklists and other eligibility lists via the self-serve Key-Value Store API
A key-value store is an off-chain list of address → value entries that a campaign reads on every reward
computation. They back the dynamic eligibility lists described in
Customization options — whitelists, blacklists,
boosts and referrer maps — and changes propagate within roughly two hours, with no on-chain transaction.
This page is the technical reference: the endpoints you call and the bodies you send to create a store and manage its entries. For the product logic — what whitelists, blacklists and referral programs do, how precedence works, and how a store is wired to a campaign — see Customization options.
Historically, Merkl provisioned every store for you. Allowlisted integrators can now create and manage their own stores programmatically with their API key — the self-serve flow documented below.
Prerequisites
- An API key. All self-serve requests authenticate with the
X-API-Keyheader — see Auth & rate limits. The store you create is owned by your key's address; only that key can write to it. - A creator quota. Self-serve creation is allowlisted. The Merkl team grants your address a quota that bounds:
maxConfigs— how many stores you may own,maxSizeLimit— the largest a single store may be (number of entries),allowedTypes— which store types you may create (e.g.WHITELIST,BLACKLIST,BOOST,REFERRER).
To request or adjust a quota, contact the Merkl team with your address and the lists you intend to manage. Without a
quota, creation returns 403.
Creating a store
POST /v4/key-value-stores/self
Headers:
X-API-Key: your-api-key
Content-Type: application/json
Body:
{
"key": "my-whitelist",
"type": "WHITELIST",
"sizeLimit": 50000,
"publicRead": true
}
| Field | Required | Notes |
|---|---|---|
key | yes | A short suffix for your store (^[a-zA-Z0-9_-]+$). It is namespaced to your address — see below. |
type | yes | One of your quota's allowedTypes. Determines the JSON shape of entry values. |
sizeLimit | no | Max entries. Defaults to min(100000, maxSizeLimit); must not exceed your quota's maxSizeLimit. |
publicRead | no | If true (default), anyone can read the entries; if false, only you can. |
You do not set the owner or access mode — the server forces the store's owner to your API key's address and its
access mode to API_KEY (only your key can write entries).
The response returns the store's canonical key — your address joined to your suffix, e.g.
0xAbC…DeF_my-whitelist — along with the resolved type, accessMode (API_KEY), sizeLimit, publicRead
and ownerAddress.
Use the returned canonical key for every later call — adding entries, updating, deleting. The key you
submitted is only the suffix. Namespacing guarantees your keys never collide with another integrator's.
Managing your stores
| Action | Endpoint |
|---|---|
| List your stores | GET /v4/key-value-stores/self |
| Update a store | PATCH /v4/key-value-stores/self/{key} |
| Delete a store (and its entries) | DELETE /v4/key-value-stores/self/{key} |
{key} is the canonical key from the create response. PATCH accepts description, publicRead and sizeLimit
(still bounded by your quota, and never below the store's current entry count). You can only manage stores you own.
Adding and removing entries
Once a store exists, manage its membership with the standard entry endpoints, authenticated with the same API key:
PUT /v4/key-value-stores/{key}/entries/batch — {key} is your canonical key; up to 1000 entries per request,
upsert.
Headers:
X-API-Key: your-api-key
Content-Type: application/json
Body:
{
"entries": [
{ "address": "0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701", "value": "{\"isWhitelisted\":true}" }
]
}
Use DELETE /v4/key-value-stores/{key}/entries/{address} to remove a single entry. The value is a JSON string,
and its shape must match the store type exactly — it is validated at write time, so a malformed body fails
immediately with a 400:
| Type | value | Meaning |
|---|---|---|
WHITELIST | {"isWhitelisted": true} | address is eligible |
BLACKLIST | {"isBlacklisted": true} | address is excluded |
BOOST | {"boostBigInt": "<bigint>"} | boost for address, base 9 (1× = "1000000000"), strictly positive |
REFERRER | {"referrer": "0x..."} | address is the invitee, value.referrer their referrer |
FORWARDER | {"forwardTo": "0x..."} | forward address's rewards to forwardTo |
Addresses are stored lowercased, so request casing doesn't matter. Writes are rejected once the store reaches its
sizeLimit. Full endpoint details are in the API reference.
Reading entries
GET /v4/key-value-stores/{key}/entries lists a store's entries, paginated with page and pageSize (defaulting to
0 and 100). Pass value to filter by an exact value match.
curl "https://api.merkl.xyz/v4/key-value-stores/{key}/entries?page=0&pageSize=100"
Response:
{
"entries": [
{
"address": "0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701",
"value": "{\"isWhitelisted\":true}",
"createdAt": "2026-06-12T09:00:00.000Z",
"updatedAt": "2026-06-12T09:00:00.000Z"
}
],
"total": 1,
"page": 0,
"pageSize": 100
}
Fetch a single entry with GET /v4/key-value-stores/{key}/entries/{address} (returns 404 if absent).
If the store was created with publicRead: true (the default), anyone can list its entries without authentication —
convenient for a public referrer map, but avoid it for a whitelist backing a private LP deal. Otherwise reads require
the owner's X-API-Key, the same key used to write entries.
Access modes: who can write
Every store has an access mode that governs who may write its entries:
| Access mode | Who can write | Batch upsert | How it's created |
|---|---|---|---|
API_KEY | Only the owner's API key — for any address | Yes (≤1000/request) | Self-serve (above) or by the Merkl team |
PUBLIC | Any wallet, but only its own entry (via wallet signature) | No | By the Merkl team only |
Self-serve stores are always API_KEY: only your key writes, and it may write any address — that's what powers bulk whitelists, referrer maps, or a creator-managed forwarding store. PUBLIC stores are the exception: Merkl provisions them so that end users can manage their own entry directly, authenticated by a wallet signature rather than the store owner's key. The canonical example is the global forwarding store below.
The global forwarding store
global-forwarding is a single, Merkl-owned PUBLIC store of type FORWARDER. It lets any wallet redirect its own Merkl rewards to another address across every campaign at once — see the user-facing guide. Each entry is address → {"forwardTo": "0x..."}.
Because it is PUBLIC, writes do not use the store owner's API key. Each wallet authenticates with a JWT from the wallet-signature login flow and may only write its own entry — a write targeting any other address returns 403. Batch upsert is not available on PUBLIC stores; write one entry at a time.
ADDR=0xYourAddress
API=https://api.merkl.xyz/v4
# 1. Get the message to sign
curl "$API/auth/nonce/$ADDR"
# 2. Sign the returned `message` with your wallet (personal_sign), then log in
# (stores a jwt cookie). Connecting your wallet on the Merkl app runs this same flow.
curl -X POST "$API/auth/login" -c cookies.txt -H 'Content-Type: application/json' \
-d '{"address":"'$ADDR'","message":"<message from step 1>","signature":"0x<sig>"}'
# 3. Enable forwarding for your own address
curl -X PUT "$API/key-value-stores/global-forwarding/entries" -b cookies.txt \
-H 'Content-Type: application/json' \
-d '{"address":"'$ADDR'","value":"{\"forwardTo\":\"0xDestination\"}"}'
# Disable forwarding
curl -X DELETE "$API/key-value-stores/global-forwarding/entries/$ADDR" -b cookies.txt
Creator-managed forwarding stores
To forward rewards on behalf of others — e.g. routing a vault's or contract's rewards to the real beneficiaries — a campaign creator uses their own FORWARDER store instead of the global one: create it via self-serve (or have Merkl provision it), push entries in bulk with your API key, and reference its canonical key from the campaign's forwarding hook. Because it is API_KEY mode, you can write any address and use batch upsert. See Customization options for the campaign wiring and precedence rules.
Wiring a store to a campaign
Creating a store does not by itself affect any campaign. A campaign must reference the store's canonical key in its hook for the engine to apply it. See Customization options for how whitelists/blacklists are attached and how precedence works (a non-empty whitelist implicitly excludes every other address), and for the referral boost semantics.
Common errors
| Status | Cause |
|---|---|
401 | Missing or invalid X-API-Key. |
403 — not allowlisted | Your address has no creator quota. |
403 — type not allowed | type is not in your quota's allowedTypes. |
403 — quota reached | You already own maxConfigs stores. |
403 — not owner | The API key doesn't belong to the store's owner (writes, or reads on a private store). |
404 — unknown store | No store exists for that key. |
404 — entry not found | GET .../entries/{address} on an address with no entry. |
400 — malformed value | The value doesn't match the store type's required shape. |
400 — size limit reached | A write would push the store past its sizeLimit. |
400 — sizeLimit exceeds quota | Requested sizeLimit is above your maxSizeLimit. |
400 — key already exists | You already have a store with that suffix. |
400 — sizeLimit below entry count | You tried to lower sizeLimit below the store's current number of entries. |