Skip to main content

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 allowance value 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).
The 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.
MySQL is the same shape with 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):
Postgres, refresh the projection, then read the live allowances:

Modes

Shipped defaults: ClickHouse hybrid (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 of GET /wallets/{address}/approvals with no on-chain source are intentionally omitted:
  • value_formatted: needs token decimals; scale value by 10^token_decimals using a Token Metadata sync.
  • token.name / token.symbol / token.logo / token.decimals, token metadata, not in tokenApprovals.
  • spender.entity / spender.entity_logo / spender.address_label, off-chain spender labelling.

Migrating from the REST API

The active allowance set, allowance value, spender, token address, block and tx, comes from this recipe directly: the latest approve per (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, decimals for value_formatted
  • Token Balances by Wallet: the wallet’s current balance of each approved token
  • Token Prices: usd_price, and usd_at_risk derived from it
Field mapping, exact comes from the chain, calculated is derived from real onchain trades (very close; compare with a small tolerance), add yourself is an off-chain signal: 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):
Query it per wallet with 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 / hybrid from 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 with usd_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 tokenApprovals feed (Kafka / AMQP / SQS) to alert the moment a risky allowance is granted, instead of polling.

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.