Question it answers
“What ERC-20 allowances has wallet 0x… granted, and to whom?” Mirrors Moralis GET /wallets/{address}/approvals.
Each ERC-20 Approval(owner, spender, value) log is one approval event. The current allowance for an (owner, token, spender) triple is the latest approval by (block_number, log_index), a fresh approve overwrites the prior allowance (latest-wins, the same machinery as the balances recipes). A revoke is just approve(spender, 0), so it lands as a new event whose value is '0'.
What you get
One row per approval event, keyed by the approving wallet (owner_address). The current allowance for a triple is the latest event, resolved with argMax (ClickHouse) or a latest-wins projection (Postgres / MySQL):
Compare
value against '0' for the revoked / non-revoked distinction; do any arbitrary-precision arithmetic in your application layer.
Source
The transform reads a single per-block array and lands one row per approval log:tokenApprovals
The struct carries approverAddress (→ owner_address), spenderAddress, tokenAddress, and amount (→ value); block_number and block_timestamp come from the block envelope.
Destination
ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct. The fact table’s sort key is owner-first, so “all allowances granted by owner Y” is a contiguous range read. Postgres keeps a flat event table plus a
token_allowances materialized view (latest approve per triple, refreshed on a schedule); MySQL keeps the same event table plus a latest-wins state table.
Full schema
Below is the complete read table this recipe produces, keep the columns you need and drop the rest (see Schema & flexibility). The raw allowancevalue is stored as text on all three destinations: an unlimited approve uses type(uint256).max (78 digits), which overflows Postgres NUMERIC(76,0) and MySQL DECIMAL(65,0).
ClickHouse, fact_token_approvals
ClickHouse, fact_token_approvals
sign column drives reorg collapsing. Read the current allowance with argMax(value, (block_number, log_index)) over FINAL, never a bare WHERE sign = 1. A single-node setup can use CollapsingMergeTree(sign) without the replication path.Postgres, token_approval_events + token_allowances
Postgres, token_approval_events + token_allowances
VARCHAR(80) for value and a trigger-maintained token_allowances state table doing the latest-wins upsert. position is the block-level cursor used during backfill.Example reads
All current allowances granted by an owner, latest approve per token + spender, dropping revoked ('0') rows (ClickHouse):
Modes
Shipped defaults: ClickHousehybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse, see the overview.
Realtime / hybrid on Postgres / MySQL is constrained: the block-level cursor means array-expanded approval rows share a
position, which collides with the single-column UNIQUE requirement. Run realtime / hybrid on ClickHouse, which corrects reorgs per-block via the collapsing log table; the Postgres / MySQL configs target historical backfill.EVM only
tokenApprovals is an EVM ERC-20 Approval log array. SPL token delegation on Solana is a different model and is not emitted into this array.
Fidelity gaps
On-chain primitives, owner, spender, token, raw allowance value, block, and tx hash, are fully covered. Response fields ofGET /wallets/{address}/approvals with no on-chain source are intentionally omitted:
value_formatted: needs tokendecimals; scalevalueby10^token_decimalsusing a Token Metadata sync.token.name/token.symbol/token.logo/token.decimals, token metadata, not intokenApprovals.spender.entity/spender.entity_logo/spender.address_label, off-chain spender labelling.
Migrating from the REST API
(owner, token, spender) wins, and revokes (approve-to-zero) drop out. The rest of the endpoint’s response (token metadata, wallet balance, USD figures) does not live on the Approval log, you rebuild it with joins.
This endpoint needs four recipes. Data Feeds are not 1:1 endpoint replicas, to reproduce the full response, run these together and join their tables yourself (query below):
- Token Approvals (this page), allowance, spender, token address, block / tx
- Token Metadata:
token.name,token.symbol,decimalsforvalue_formatted - Token Balances by Wallet: the wallet’s current balance of each approved token
- Token Prices:
usd_price, andusd_at_riskderived from it
The join (Postgres; table names follow each recipe’s default schema), one view that reproduces the response, with
usd_at_risk computed the realistic way (min(allowance, balance) × price, a spender can’t take more than the wallet holds):
ORDER BY usd_at_risk DESC NULLS LAST so the riskiest approvals surface first.
Good to know
- Backfill full history first. The latest approve for a triple can sit at any past block, run
historical/hybridfrom an early block or you’ll miss or misstate allowances (History & backfill). - Unlimited approvals show as a max-uint
value(78 digits, stored as text). Lead withusd_at_risk, it caps the scary figure to what the wallet actually holds. - Revokes disappear automatically (an approve to zero), so you always see the live, active set.
- Compare USD figures with a tolerance: prices come from real DEX trades, not the REST API’s pricing.
- Real-time is the strength here. Stream the
tokenApprovalsfeed (Kafka / AMQP / SQS) to alert the moment a risky allowance is granted, instead of polling.
Related
Wallet History
The full chronological event feed, approvals included alongside transfers and swaps.
Compliance & AML
Outstanding allowances are a core risk surface for wallet monitoring.

