Skip to main content
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 transfer memos with a Turbo pipeline, 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:
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

  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 installed and logged in to your project.
  • A Postgres database and a Goldsky secret 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 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 pattern.
reconciliation-pipeline.yaml
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:
goldsky turbo apply reconciliation-pipeline.yaml
goldsky turbo inspect reconciliation-pipeline.yaml -n settlements
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.

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.
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.
Use the PostgreSQL aggregate sink to maintain a running, real-time rollup - for example a running total settled per token - without a scheduled job.
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

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

Can’t find what you’re looking for? Reach out to us at support@goldsky.com for help.