Every trade of a Zora coin pays several people at once: the coin’s creator, the platform that launched it, the app that routed the trade, the protocol, and Doppler. If you create coins, run a platform that launches them, or build an app that passes a referrer on trades, some of that money is yours. Zora’s REST API won’t tell you how much. The payouts are onchain, one event per trade, and getting yours out takes one idea and some patience.
The event, and the problem with it
Coins launched since Zora moved to Uniswap V4 pay rewards through their hook, which emits CoinMarketRewardsV4. Its payload is seven addresses (the coin, the currency the reward is paid in, then the creator, platform referrer, trade referrer, protocol and Doppler recipients) followed by ten amounts: for each of those five recipients, what they got in the currency and what they got in the coin itself.
None of those fields is declared indexed, and that one detail decides everything. A log’s indexed fields go into its topics, and topics are what eth_getLogs filters on. This event has a single topic, its own signature. So you can ask a node for every CoinMarketRewardsV4 event in a block range, but not for the ones that pay you. Your address is in the data, where the node doesn’t look.

Coins from before the V4 migration use an older event, CoinTradeRewards, which does index its three recipients, so an ordinary address filter works for them. For anything launched since mid-2025, it doesn’t.
So read all of them
The fix is unglamorous: fetch every reward event in a range, decode it, and keep the ones where your address is one of the three recipients. On Base today that’s about 400 events per 2,000 blocks. Here’s the whole thing with nothing but JSON-RPC:
import httpx
RPC = "https://mainnet.base.org"
# keccak256 of the CoinMarketRewardsV4 event signature: its one and only topic
TOPIC = "0x35b5031218696db1dfd903223a47f38e66a1998e14a942a5d60fddaa49a685fc"
ME = "0x55c88bb05602da94fce8feadc1cbebf5b72c2453"
def rpc(method, params):
body = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
return httpx.post(RPC, json=body, timeout=30).json()["result"]
head = int(rpc("eth_blockNumber", []), 16)
logs = rpc("eth_getLogs", [{"fromBlock": hex(head - 2000), "toBlock": hex(head),
"topics": [TOPIC]}])
addr = lambda word: "0x" + word[-20:].hex()
for log in logs:
data = bytes.fromhex(log["data"][2:])
w = [data[i:i + 32] for i in range(0, len(data), 32)]
# words 0-6: addresses; 7-16: amounts (per role, in currency then in the coin)
roles = [("creator", w[2], w[7]), ("platform referrer", w[3], w[9]),
("trade referrer", w[4], w[11])]
for role, who, amount in roles:
if addr(who) == ME:
block = int(log["blockNumber"], 16)
print(block, role, int.from_bytes(amount, "big"), "of", addr(w[1]))
The data is plain ABI encoding: 32-byte words, addresses right-aligned in the first seven, amounts as big-endian integers in the other ten. Amounts are in the token’s smallest unit, so 486910 of USDC (0x8335…2913) is 0.486910 USDC, and 18-decimal tokens like ZORA and WETH need dividing by 1018.
Scanning a real window
One call covers 2,000 blocks. Base makes a block every two seconds, so that’s about 67 minutes; a day is 43,200 blocks (22 calls) and 30 days is about 650 calls. A few details make that practical:
- Time is arithmetic. Base blocks are exactly two seconds apart, so a block’s timestamp is
1686789347 + 2 × block. You don’t need aneth_getBlockByNumberper event to date it, and “the last seven days” is a subtraction from the head block. - Start where the events start. The first V4 reward events show up around block 31,000,000, in June 2025. There’s nothing to find before that.
- Expect range errors. Public RPCs cap the block range or the number of results per call. When one refuses, halve the range and retry instead of failing the whole scan.
- Remember what you’ve scanned. Store the block ranges you’ve covered for each address and only fetch what’s new next time. The second run of a 30-day report should take seconds.
- Price it at the end. Rewards arrive in ZORA, WETH, USDC or a creator coin. Sum raw amounts per role and token first, and convert to dollars once, when you report.
Or use the library
All of that is the rewards module in the Zora Coins SDKs, which are open source and MIT licensed. In Python:
from zora_coins.rewards import RewardsIndexer, build_report, to_text
me = "0x55c88bb05602da94fce8feadc1cbebf5b72c2453"
with RewardsIndexer("rewards.sqlite") as idx:
idx.scan([me], days=7) # re-running only fetches blocks it hasn't seen
print(to_text(build_report(idx.events_for([me]), [me])))
For about an hour and a half of blocks, that printed:
Zora rewards for 0x55c88bb05602da94fce8feadc1cbebf5b72c2453
114 reward events, blocks 51477712–51479837
Platform referral 145.7255 ZORA $1.18 (7 payouts)
Platform referral 2.1861 USDC $2.19 (25 payouts)
Trade referral 6.7341 ZORA $0.05 (69 payouts)
Trade referral 6.21437e-05 WETH $0.16 (14 payouts)
Trade referral 0.175478 USDC $0.18 (14 payouts)
Total (current prices) $3.76
The same indexer is in the TypeScript, Go, Rust, C#, Java and C++ SDKs, and the Python, TypeScript, Go and Rust packages include it as a zora-rewards command (pip install zora-coins, then zora-rewards --days 7 0xYourAddress). All seven give the same totals, to the wei, on the same set of recorded events. That cross-check is how I know the word offsets above are right.
Unofficial
The SDKs are community-maintained by Rebel Studios and not affiliated with Zora. If Zora changes the event, the recorded-event tests will be the first thing to fail, and the fix will go out as a new version in every language at once.