> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moralis.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Token Approvals

> Sync the current ERC-20 allowances a wallet has granted, and to whom, as a continuous, reorg-safe feed into your own database. Mirrors the Moralis GET /wallets/{address}/approvals endpoint.

### 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 templates). 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):

| Column                         | Description                                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------------------------- |
| `owner_address`                | The approving wallet (the `owner` on the `Approval` log)                                          |
| `spender_address`              | The address authorized to spend                                                                   |
| `token_address`                | The ERC-20 token the allowance is for                                                             |
| `value`                        | The raw allowance amount, stored as text (an unlimited approve is `type(uint256).max`, 78 digits) |
| `block_number`, `log_index`    | On-chain ordering tuple; the latest pair per triple wins                                          |
| `tx_hash`                      | Transaction that produced the approval                                                            |
| `event_ts` / `block_timestamp` | Block time                                                                                        |

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

| Destination                  | Table                                                               | Read pattern                                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **ClickHouse** (first-class) | `fact_token_approvals`                                              | Prefix scan on `(chain_id, owner_address, token_address, spender_address, …)`; current allowance via `argMax(value, (block_number, log_index))` over `FINAL` |
| **Postgres**                 | `token_approval_events` + `token_allowances` materialized view      | Partial index `(owner_address, token_address, spender_address) WHERE value <> '0'`                                                                           |
| **MySQL**                    | `token_approval_events` + maintained `token_allowances` state table | PK `(owner_address, token_address, spender_address)` with `value = '0'` cleanup                                                                              |

ClickHouse uses the collapsing log-table pattern (see the [templates overview](/data-feeds/templates/overview#destinations)) 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 template produces, keep the columns you need and drop the rest (see [Schema & flexibility](/data-feeds/templates/overview#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)`.

<Accordion title="ClickHouse, fact_token_approvals">
  ```sql theme={null}
  CREATE TABLE recipe_token_approvals.fact_token_approvals
  (
      vendor_event_id   String,
      ingested_at       DateTime64(3),
      chain_id          UInt32,
      block_hash        String,
      block_number      UInt64,
      log_index         UInt32,
      event_ts          DateTime64(3),
      tx_hash           String,
      token_address     String,
      owner_address     String,
      spender_address   String,
      value             String,                 -- raw allowance; unlimited approve = uint256 max (78 digits)
      sign              Int8
  )
  ENGINE = ReplicatedCollapsingMergeTree(
      '/clickhouse/tables/{database}/fact_token_approvals', '{replica}', sign)
  PARTITION BY (chain_id, toYYYYMM(event_ts))
  ORDER BY (chain_id, owner_address, token_address, spender_address, block_number, log_index, vendor_event_id);
  ```

  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.
</Accordion>

<Accordion title="Postgres, token_approval_events + token_allowances">
  ```sql theme={null}
  -- 1. Approval events (sink target) — one row per Approval log.
  CREATE TABLE public.token_approval_events (
    position         BIGINT  NOT NULL,
    log_index        BIGINT  NOT NULL,
    block_number     BIGINT  NOT NULL,
    block_timestamp  BIGINT  NOT NULL,          -- unix seconds
    tx_hash          TEXT    NOT NULL,
    token_address    TEXT    NOT NULL,
    owner_address    TEXT    NOT NULL,
    spender_address  TEXT    NOT NULL,
    value            TEXT    NOT NULL,           -- allowance; max-uint = 78 digits
    vendor_event_id  TEXT    NOT NULL
  );

  -- Recency index leading with the DISTINCT ON keys so REFRESH avoids a sort.
  CREATE INDEX tae_owner_token_spender_recency_idx
    ON public.token_approval_events
    (owner_address, token_address, spender_address, block_number DESC, log_index DESC);

  -- 2. Current-allowance materialized view: latest approve per (owner, token, spender).
  CREATE MATERIALIZED VIEW public.token_allowances AS
  SELECT DISTINCT ON (owner_address, token_address, spender_address)
    owner_address, token_address, spender_address,
    value, tx_hash, block_number, log_index, block_timestamp
  FROM public.token_approval_events
  ORDER BY owner_address, token_address, spender_address, block_number DESC, log_index DESC;

  CREATE UNIQUE INDEX token_allowances_pk
    ON public.token_allowances (owner_address, token_address, spender_address);

  -- Primary access path: all live allowances granted by an owner.
  CREATE INDEX token_allowances_by_owner_active_idx
    ON public.token_allowances (owner_address, token_address, spender_address)
    WHERE value <> '0';

  -- Sibling access path: who can spend a given token on behalf of others.
  CREATE INDEX token_allowances_by_spender_idx
    ON public.token_allowances (spender_address, token_address);
  ```

  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.
</Accordion>

### Example reads

All current allowances granted by an owner, latest approve per token + spender, dropping revoked (`'0'`) rows (ClickHouse):

```sql theme={null}
SELECT token_address,
       spender_address,
       argMax(value, (block_number, log_index)) AS current_allowance
FROM recipe_token_approvals.fact_token_approvals FINAL
WHERE chain_id = 1 AND owner_address = lower('0x...')
GROUP BY token_address, spender_address
HAVING current_allowance != '0' AND current_allowance != ''
ORDER BY token_address, spender_address;
```

Postgres, refresh the projection, then read the live allowances:

```sql theme={null}
REFRESH MATERIALIZED VIEW CONCURRENTLY token_allowances;

SELECT token_address, spender_address, value
FROM public.token_allowances
WHERE owner_address = lower('0x...') AND value <> '0'
ORDER BY token_address, spender_address;
```

### Modes

Shipped defaults: **ClickHouse `hybrid`** (backfill → realtime), **Postgres / MySQL `historical`** (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse, see the [overview](/data-feeds/templates/overview#modes).

<Note>
  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.
</Note>

### 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

```
GET /wallets/:address/approvals
```

The **active allowance set**, allowance value, spender, token address, block and tx, comes from this template 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.

<Note>
  **This endpoint needs four templates.** 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](/data-feeds/templates/token/token-metadata): `token.name`, `token.symbol`, `decimals` for `value_formatted`
  * [Token Balances by Wallet](/data-feeds/templates/wallet/token-balances-by-wallet): the wallet's current balance of each approved token
  * [Token Prices](/data-feeds/templates/token/token-prices): `usd_price`, and `usd_at_risk` derived from it
</Note>

**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:

| `/wallets/:address/approvals`                                       | Data Feeds                                | Fidelity   |
| ------------------------------------------------------------------- | ----------------------------------------- | ---------- |
| `value` (allowance)                                                 | `token_allowances.value`                  | exact      |
| `value_formatted`                                                   | derive: `value / 10^decimals`             | exact      |
| `block_number`, `block_timestamp`, `transaction_hash`               | `token_allowances.*`                      | exact      |
| `spender.address`                                                   | `token_allowances.spender_address`        | exact      |
| `token.address`                                                     | `token_allowances.token_address`          | exact      |
| `token.name`, `token.symbol`                                        | token metadata sync                       | exact      |
| `token.current_balance(_formatted)`                                 | token balances sync                       | exact      |
| `token.usd_price`                                                   | latest price update                       | calculated |
| `token.usd_at_risk`                                                 | derive: `min(allowance, balance) × price` | calculated |
| `token.logo`, `possible_spam`, `verified_contract`, `address_label` | add yourself                              | off-chain  |
| `spender.address_label`, `entity`, `entity_logo`                    | add yourself                              | off-chain  |

**The join** (Postgres; table names follow each template'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):

```sql theme={null}
CREATE VIEW wallet_approvals AS
SELECT
  a.owner_address                                AS wallet_address,
  a.token_address,
  a.spender_address,
  m.name, m.symbol,
  a.value                                        AS allowance,
  a.value / power(10, m.decimals)                AS value_formatted,
  b.balance                                      AS current_balance,
  p.usd_price,
  least(a.value, coalesce(b.balance, 0))
    / power(10, m.decimals) * p.usd_price        AS usd_at_risk,
  a.block_number, a.block_timestamp, a.tx_hash
FROM token_allowances a
LEFT JOIN token_metadata m ON m.token_address = a.token_address
LEFT JOIN token_balances b ON b.token_address = a.token_address
                          AND b.wallet_address = a.owner_address
LEFT JOIN LATERAL (
  SELECT usd_price FROM token_price_updates
  WHERE token_address = a.token_address ORDER BY block_number DESC LIMIT 1
) p ON true;
```

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](/data-feeds/concepts/history-and-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.

### Related

<Columns cols={2}>
  <Card title="Wallet History" href="/data-feeds/templates/wallet/wallet-history" icon="clock-rotate-left">
    The full chronological event feed, approvals included alongside transfers and swaps.
  </Card>

  <Card title="Compliance & AML" href="/data-feeds/use-cases/compliance-aml" icon="shield-check">
    Outstanding allowances are a core risk surface for wallet monitoring.
  </Card>
</Columns>
