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

# Real-time payment reconciliation

> Decode TIP-20 transfer memos with Turbo and match onchain settlements to your ledger in real time.

Traditional payment reconciliation is a batch, T+1/T+2 process: at the end of the day, an operations team matches a settlement file against the ledger and chases the breaks by hand. It is slow, expensive, and it hides problems until the next morning.

Stablecoin settlement on a payments chain removes the delay from the *movement* of money. This guide removes the delay from the *accounting* for it. You will decode [TIP-20](https://tempo.xyz/blog/tip20/) transfer memos with a [Turbo pipeline](/turbo-pipelines/introduction), stream them into your own ledger database, and match each settlement to an invoice on its memo - continuously, in seconds.

## Why the memo is the missing piece

A plain ERC-20 `Transfer` tells you *who paid whom, how much*. It does not tell you *what for*. That single missing field is why onchain payments have historically needed a separate, off-chain mapping to be reconcilable at all.

TIP-20 adds it at the protocol level. Every payment can carry a 32-byte memo:

```solidity theme={null}
event TransferWithMemo(
    address indexed from,
    address indexed to,
    uint256 amount,
    bytes32 indexed memo   // your invoice ID, order number, or PSP reference
);
```

Put your reference in the memo when you initiate the payment, and reconciliation stops being a fuzzy amount-and-timestamp match. It becomes a deterministic join on a key you control.

## How it works

```mermaid theme={null}
flowchart LR
    A[TIP-20 token on Tempo] -->|TransferWithMemo events| B[Turbo pipeline]
    B -->|decode + reshape| C[(Ledger DB<br/>onchain_settlements)]
    D[(Your invoices<br/>orders / AR)] --> E{Match on memo}
    C --> E
    E -->|matched| F[Reconciled]
    E -->|no match / short pay| G[Exception queue]
```

1. **Stream** TIP-20 logs from Tempo with Turbo.
2. **Decode** `TransferWithMemo` on the fly using the token's ABI.
3. **Write** each settlement - including the memo - into a `onchain_settlements` table co-located with your invoices.
4. **Match** settlements to invoices on the memo. Anything unmatched or short-paid surfaces as an exception the instant it settles.

## Prerequisites

* The [Turbo CLI extension](/turbo-pipelines/cli#installation) installed and logged in to your project.
* A Postgres database and a [Goldsky secret](/turbo-pipelines/pipeline-config#secrets) with its connection string.
* The contract address of the TIP-20 token you settle in, and its ABI. Confirm the exact Tempo dataset name in the [Datasource explorer](https://app.goldsky.com/data-sources) or with `goldsky dataset list`.

## Step 1: Stream and decode settlements

TIP-20 events are ordinary Solidity logs, so they land in the `raw_logs` dataset and can be decoded inside the pipeline. This mirrors the [decode custom contract events](/turbo-pipelines/sources/evm#guide-decode-custom-contract-events) pattern.

```yaml reconciliation-pipeline.yaml expandable theme={null}
name: tip20-reconciliation
resource_size: s

sources:
  tempo_tip20_logs:
    type: dataset
    dataset_name: tempo.raw_logs
    version: 1.0.0
    start_at: latest
    # Fast-scan: only ingest logs from your settlement token
    filter: address = lower('0xYOUR_TIP20_TOKEN')

transforms:
  # 1. Decode the raw log with the TIP-20 ABI
  decoded_payments:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        address AS token,
        _gs_log_decode(
          _gs_fetch_abi('https://raw.githubusercontent.com/your-org/abis/main/tip20.json', 'raw'),
          topics,
          data
        ) AS decoded,
        block_number,
        block_timestamp,
        transaction_hash,
        _gs_op
      FROM tempo_tip20_logs
      WHERE address = lower('0xYOUR_TIP20_TOKEN')

  # 2. Keep only TransferWithMemo events and flatten the fields
  settlements:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        token,
        decoded.event_params[1]                     AS from_address,
        decoded.event_params[2]                     AS to_address,
        CAST(decoded.event_params[3] AS DECIMAL(38, 0)) AS amount,
        decoded.event_params[4]                     AS memo,
        to_timestamp(block_timestamp)               AS settled_at,
        transaction_hash,
        _gs_op
      FROM decoded_payments
      WHERE decoded IS NOT NULL
        AND decoded.event_signature = 'TransferWithMemo'

sinks:
  ledger_settlements:
    type: postgres
    from: settlements
    schema: ledger
    table: onchain_settlements
    secret_name: MY_POSTGRES
    primary_key: id
```

Deploy and watch it live:

```bash theme={null}
goldsky turbo apply reconciliation-pipeline.yaml
goldsky turbo inspect reconciliation-pipeline.yaml -n settlements
```

<Tip>
  Don't want to hardcode the ABI? `_gs_fetch_abi` can pull it from a block explorer with an Etherscan-compatible API (`_gs_fetch_abi('<explorer-url>', 'etherscan')`). Fetching keeps the pipeline in sync if the token's interface is upgraded.
</Tip>

## Step 2: Reconcile against your ledger

Your `onchain_settlements` table now fills in real time, next to your existing `invoices` table. Because you control the memo, matching is a single join. Encode your invoice reference into the 32-byte memo when you create the payment (for example a zero-padded ID or a hash), and store the same encoding on the invoice.

```sql theme={null}
SELECT
  i.invoice_id,
  i.amount_due,
  s.amount        AS amount_settled,
  s.settled_at,
  s.transaction_hash,
  CASE
    WHEN s.id IS NULL                    THEN 'unpaid'
    WHEN s.amount >= i.amount_due        THEN 'reconciled'
    WHEN s.amount <  i.amount_due        THEN 'short_paid'
  END AS status
FROM ledger.invoices i
LEFT JOIN ledger.onchain_settlements s
  ON lower(s.memo) = lower(i.memo_ref)
WHERE i.status = 'open';
```

Everything that returns `unpaid` past its due date, `short_paid`, or arrives with a memo that matches no invoice (an *unexpected* payment) is a reconciliation break - surfaced the moment it settles, not the next morning.

## Step 3: Make exceptions live

Two options turn the query above into an always-on control.

<Tabs>
  <Tab title="Live exception totals (Postgres aggregate)">
    Use the [PostgreSQL aggregate sink](/turbo-pipelines/sinks/postgres-aggregate) to maintain a running, real-time rollup - for example a running total settled per token - without a scheduled job.

    ```yaml theme={null}
    sinks:
      settled_totals:
        type: postgres_aggregate
        from: settlements
        schema: ledger
        landing_table: settlements_log
        agg_table: settled_totals
        primary_key: id
        secret_name: MY_POSTGRES
        group_by:
          token:
            type: text
        aggregate:
          total_settled:      # running sum, maintained by a DB trigger
            from: amount
            fn: sum
            type: numeric(38,0)
          settlement_count:
            fn: count
    ```
  </Tab>

  <Tab title="Instant break alerts (webhook)">
    Send unmatched settlements straight to your ops tooling. Add a transform that flags settlements whose memo is not in your open-invoice set (a [dynamic table](/turbo-pipelines/transforms/dynamic-tables) of expected references), then route it to a [webhook sink](/turbo-pipelines/sinks/webhook):

    ```yaml theme={null}
    transforms:
      # Expected payment references, kept in sync from your invoicing system
      open_invoices:
        type: dynamic_table
        backend_type: Postgres
        backend_entity_name: open_invoices
        secret_name: MY_POSTGRES

      # A settlement whose memo matches no open invoice is a break
      unmatched_settlements:
        type: sql
        primary_key: id
        sql: |
          SELECT
            id,
            token,
            from_address,
            amount,
            memo,
            settled_at,
            transaction_hash,
            _gs_op
          FROM settlements
          WHERE NOT dynamic_table_check('open_invoices', lower(memo))

    sinks:
      # Only unmatched settlements reach the break URL - reconciled payments do not
      break_alerts:
        type: webhook
        from: unmatched_settlements
        url: https://ops.example.com/reconciliation/break
        one_row_per_request: true
        secret_name: OPS_WEBHOOK_AUTH
    ```

    Keep `open_invoices` current from your invoicing system - insert a reference when an invoice opens, delete it when it is paid or cancelled - so the break feed carries only genuinely unexpected settlements.
  </Tab>
</Tabs>

## Step 4: Trust your reads

Reconciliation is only as reliable as the data feeding it. Point the indexers and verification jobs behind this workflow at [Edge RPC](/edge-rpc/why-edge):

* **Cross-validation and block-range enforcement** mean you never reconcile against a missing log or a gap in `eth_getLogs`.
* **Automatic failover** keeps the ledger current through provider outages.

On Tempo specifically, transactions reach **sub-second, re-org-free finality**, so a settlement you record will not later be unwound - the property tradfi back-offices have always wanted from a settlement rail.

## Optional: auto-resolve breaks with Compose

For breaks that can be handled programmatically - retrying a failed payout, issuing a refund for an overpayment, or opening a ticket - trigger a [Compose](/compose/introduction) task from the exception. Compose gives you **durable execution** (the action completes even through failures) and a **full trace** of every step, so each automated resolution is auditable. See [task triggers](/compose/task-triggers) to fire a task from a webhook or onchain event.

## Business outcomes

* **Reconciliation moves from overnight to real time.** Breaks surface in seconds, not the next business day.
* **Fewer manual matches.** A deterministic memo join replaces amount-and-timestamp guesswork, shrinking the exception queue.
* **Faster cash application.** Payments apply to invoices as they settle, tightening working-capital cycles.
* **A clean audit trail.** Every settlement, its memo, and its match status live in your own database.

## Resources

* [Decode custom contract events with Turbo](/turbo-pipelines/sources/evm#guide-decode-custom-contract-events)
* [PostgreSQL aggregate sink](/turbo-pipelines/sinks/postgres-aggregate)
* [Stablecoin transfers guide](/mirror/guides/token-transfers/stablecoin-transfers)
* [Tempo chain reference](/chains/tempo)

Can't find what you're looking for? Reach out to us at [support@goldsky.com](mailto:support@goldsky.com) for help.
