A "who earned what" CSV request is the most under-estimated job in on-chain data. It sounds like eth_getLogs plus a groupBy. It is not, and the ways it breaks are specific and repeatable.
I got tired of re-deriving the same five rules on every bounty, so I wrote them into an engine: ledgerkit, MIT, ~1k lines of Python. The design goal was not "produce a CSV". It was produce a CSV and a second computation that can disagree with it.
The pipeline
fetch -> build -> verify
logs ledger reconciliation
pip install eth-abi
# 1. every log the contract ever emitted
python -m ledgerkit fetch --chain arbitrum \
--address 0xdffe8a4a4103f968ffd61fd082d08c41dcf9b940 \
--from-block 0 --out logs.json
# 2. fold them into a per-wallet ledger
python -m ledgerkit build --logs logs.json --fee-bps 250 \
--csv wallets.csv --summary summary.json
# 3. reconcile against an independent recount
python -m ledgerkit verify --logs logs.json --fee-bps 250 \
--expect-earned 3.726923002 --expect-paid 3.63374992695
The middle step writes a file, so the aggregation can be audited without touching the network.
The five rules that make the numbers right
1. Earned is gross; paid out is net. The fee is read from the settlement event, never recomputed from a percentage. Protocols change their fee rate, and old settlements keep the old one. --fee-bps is a check, and a mismatch is reported rather than silently applied. If a contract took 3% two years ago and 2.5% now, a single --fee-bps 250 pass would silently rewrite history — the engine refuses.
2. Cancelled bounties pay nobody. A settlement event on a cancelled bounty is surfaced as a warning, because it means one of the two events is wrong. Silently trusting settlements is how a CSV ends up paying someone for a bounty that was cancelled an hour before.
3. A claim without a bounty is not guessed. If the creation event falls outside the fetched block range — extremely common with a --from-block set for speed — the claim is counted in claims_accepted but excluded from the money columns, under claims_valued, and reported. Inventing an amount is how a ledger drifts from reality in a way nobody notices until an accountant does.
4. Duplicate (tx, logIndex) pairs are dropped. Retried eth_getLogs pages are the single most common source of doubled totals, and public RPCs retry constantly. Deduplicating on the pair, not on the tx hash, is the only correct key: one transaction legitimately settles several claims.
5. Exclusions are explicit and shared. --exclude-tx 0x… removes known-bad settlements (exploits, test transactions) before aggregation, and the verifier applies the same list — so the builder and the checker cannot disagree about what was excluded.
NFTs get their own rule: ownership is a last-transfer problem, not a sum. Balances are rebuilt from deltas (mint +1, burn −1, transfer −1/+1) and compared per holder.
What verify actually does
Every check is a second computation, not a re-run of the builder:
| check | method |
|---|---|
conservation |
earned == paid_out + fees, wei-exact |
raw_recount_earned / _fees
|
money columns re-derived by slicing raw data words with int(hex, 16), bypassing the ABI decoder entirely |
raw_recount_settlements |
settlement count from raw topics vs the ledger's claims_valued
|
no_negative_values, paid_out_within_earned
|
per-wallet sanity |
no_empty_rows |
every row carries activity — no phantom wallets |
fee_rate_matches |
total fees against --fee-bps, within rounding |
nft_balances |
ERC-721 balances from transfer deltas, optionally against balanceOf on chain |
Real output, against the golden fixture (3,467 logs from a live escrow contract):
ok expected earned 3.726923002 ETH, got 3.726923002
ok expected paid 3.63374992695 ETH, got 3.63374992695
ok conservation: earned=3726923002000000000 paid_out=3633749926950000000 fees=93173075050000000 diff=0
ok raw_recount_earned: raw=3726923002000000000 ledger=3726923002000000000
ok raw_recount_fees: raw=93173075050000000 ledger=93173075050000000
ok raw_recount_settlements: raw=278 ledger=278
ok no_negative_values: 0 wallet(s) below zero
ok paid_out_within_earned: 0 wallet(s) paid more than earned
ok no_empty_rows: 0 wallet(s) carry no activity
ok fee_rate_matches: expected=93173075050000000 (at 250 bps) actual=93173075050000000
ok nft_balances: 81 holder(s) from deltas vs 81 in ledger, 722 vs 722 tokens (0 token(s) with unseen history skipped)
Any mismatch exits non-zero, so it drops into CI as-is:
- run: python -m ledgerkit verify --logs logs.json --fee-bps 250 --expect-earned 3.726923002
Why integer arithmetic is not optional
Every money value stays integer wei until formatting. 3.726923002 ETH is never a float, because 3726923002000000000 in a float64 is 3726923002000000000.0 right up until it is not, and the failure shows up as a one-wei disagreement between two reports that should be identical. The verifier compares integers. That is the whole trick.
What the engine will not do for you
It does not know your protocol. Mapping an unfamiliar contract's events — which event is a settlement, which is a cancellation, whether the fee is inside the transfer or taken before it — is the actual work, and it is per-contract. The engine gives you the invariants; the mapping is judgment.
That is also why the honest version of this service is not a SaaS dashboard. It is: read the contract, write the mapping, run the ledger, hand over a reconciliation that survives a second opinion — and say out loud which rows are unvalued and why.
Where to get it
Code, tests and the golden fixture: github.com/9xj89gzrtw-hue/ledgerkit.
If you want the run rather than the tool — a per-wallet ledger for your escrow or bounty contract with the verification report attached — the scope is agreed in a public GitHub issue first, and it is paid in USDC on Base. Details and an example of the same approach applied to a different data problem (411 DefiLlama fee feeds silently reporting zero) are on the store page:
Tip jar / custom data bounties
USDC on Base, any amount, same address the store uses:
0x72Aa6edDE71B94f6b99c8e52EdC02B5C75F74d1a
Custom work I take: on-chain ledgers with reconciliation (from $149), non-standard event mappings (from $249), DefiLlama fee/revenue adapters (from $99). Open an issue with the word quote and what you need — public and timestamped, no DM required.
Top comments (0)