Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development11 min read

Crypto Gateway Architecture: Accepting On-Chain Payments Reliably

A practical engineering guide to how a crypto gateway detects, confirms, and settles on-chain payments, with runnable checks and failure-mode diagnostics.

TL;DR

A crypto gateway is the software layer that watches a blockchain for incoming transfers, decides when a payment is final enough to credit, and hands a settlement instruction to your order system. The hard parts are not the checkout button but address derivation, event detection, reorg handling, and idempotent crediting. This article explains the mental model, walks through a runnable detection and verification procedure, and shows how to diagnose the most common integration failures. It separates documented chain behavior from choices you must make and measurements you must run on your own stack.

Scope and the direct answer

A crypto gateway is a payment processor for on-chain value: it generates a deposit address or invoice, monitors the chain for a matching transfer, applies a confirmation policy, and reports a settled payment to the merchant. Some gateways also convert to stablecoins or fiat and handle payouts, but the irreducible core is detection plus finality policy plus reconciliation.

This article covers the engineering side: how detection works, how to integrate it without double-crediting, and how to verify that your gateway is actually seeing what the chain sees. Provider selection and endpoint choice are deliberately out of scope here; for concrete endpoint recommendations use RPC Assistant, and for network-level context see the multi-chain RPC endpoints guide.

  • In scope: address derivation, event detection, confirmation depth, reorg handling, idempotency, reconciliation.
  • Out of scope: licensing, card acquiring, tax treatment, and vendor pricing.
  • Assumption: you control an order system and can call an RPC endpoint or a gateway API.

Prerequisites and assumptions

The procedures below assume you have an RPC endpoint for the target chain, a database with a unique constraint on transaction identifiers, and a job runner that can poll or subscribe. They also assume you treat the gateway as untrusted input: every credited payment should be independently verifiable against chain data.

If you run your own infrastructure, a dedicated node removes noisy-neighbor effects on log queries; see dedicated node options. If you rely on a hosted endpoint, confirm its rate limits and archive depth before designing a backfill, and review RPC pricing so polling frequency does not become a surprise cost.

Mental model: invoice, watcher, ledger

Separate the gateway into three components. The invoice service creates a payment intent with an amount, an asset, an expiry, and a deposit address. The watcher reads blocks and extracts transfers that touch any address in your active set. The ledger records a state machine per payment: created, seen, confirmed, credited, or expired.

The watcher should never write directly to the ledger's credited state. It emits observations; a separate policy step decides whether an observation satisfies the confirmation rule. This split lets you change confirmation depth per asset without rewriting detection, and it makes replaying history safe.

Address strategy matters. A unique address per invoice makes matching trivial but multiplies key management. A shared address requires matching on amount and memo, which is fragile when two customers pay the same amount. Most production gateways use HD-derived unique addresses and store the derivation index alongside the invoice.

  • Invoice service: intent, amount, asset, expiry, address.
  • Watcher: block scan or log subscription, filtered by active addresses.
  • Policy: confirmation depth, value tolerance, reorg response.
  • Ledger: idempotent state transitions keyed by chain transaction hash and log index.

Detection mechanics: blocks, logs, and native transfers

Token transfers on EVM chains usually appear as event logs, so a watcher can filter by the token contract and the transfer topic rather than scanning every transaction. Native transfers do not emit logs, so they require block-level inspection or a provider method that returns balance changes. On account-based chains without logs, you generally diff balances or use the node's transaction index.

Polling is simpler to operate than subscriptions and easier to make resumable: store the last fully processed block number and continue from there. Subscriptions reduce latency but must be treated as hints, because a dropped connection silently loses events. A robust design uses subscriptions for speed and a periodic block-range sweep for completeness.

curl -s RPC_URL \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getLogs",
    "params": [{
      "fromBlock": "0x0",
      "toBlock": "latest",
      "address": "TOKEN_CONTRACT_ADDRESS",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
        null,
        "0x000000000000000000000000YOUR_DEPOSIT_ADDRESS"
      ]
    }]
  }'

Runnable verification of a single payment

Before trusting any gateway webhook, verify the underlying transaction yourself. The script below fetches a transaction and its receipt, checks the recipient and value, and prints a verdict. Replace the placeholders; never commit real keys.

Run it against the same chain your gateway claims to use. A mismatch between the gateway's reported network and the chain your RPC returns is one of the most common causes of phantom credits.

import json, urllib.request

RPC_URL = "RPC_URL"
TX_HASH = "0xTRANSACTION_HASH"
EXPECTED_TO = "0xDEPOSIT_ADDRESS".lower()
EXPECTED_WEI = 10**16  # 0.01 native unit

def rpc(method, params):
    req = urllib.request.Request(
        RPC_URL,
        data=json.dumps({"jsonrpc":"2.0","id":1,"method":method,"params":params}).encode(),
        headers={"content-type":"application/json"},
    )
    with urllib.request.urlopen(req, timeout=15) as r:
        body = json.load(r)
    if "error" in body:
        raise RuntimeError(body["error"])
    return body["result"]

tx = rpc("eth_getTransactionByHash", [TX_HASH])
receipt = rpc("eth_getTransactionReceipt", [TX_HASH])

if tx is None or receipt is None:
    print("NOT_FOUND: gateway may be reporting a different chain")
else:
    ok_to = (tx.get("to") or "").lower() == EXPECTED_TO
    ok_value = int(tx["value"], 16) >= EXPECTED_WEI
    ok_status = int(receipt["status"], 16) == 1
    print({"to_ok": ok_to, "value_ok": ok_value, "success": ok_status,
           "block": int(receipt["blockNumber"], 16)})

Confirmation policy and reorg handling

A confirmation count is a risk dial, not a guarantee. The right depth depends on the chain's observed reorg behavior, the value at stake, and your tolerance for delayed fulfillment. Documented finality rules differ by network: some chains expose an explicit finalized block tag, others rely on probabilistic depth. Use the explicit tag where it exists and a measured depth where it does not.

When a reorg invalidates a previously seen transfer, your ledger must be able to move a payment backward from confirmed to seen. That means crediting should be reversible until a finality threshold, or fulfillment should be delayed until the threshold is crossed. Never delete the observation; append a correction so audits can reconstruct what happened.

  • Store block hash with every observation so you can detect parent changes.
  • On a hash mismatch at the same height, rewind to the last common ancestor and rescan.
  • Keep a per-asset confirmation table in configuration, not in code.
  • Log the policy version that produced each credit decision.

Idempotency and reconciliation

Gateways retry webhooks, and chains occasionally surface the same transfer through both a subscription and a sweep. The only durable defense is a unique constraint on (chain_id, tx_hash, log_index) in the observations table, plus a state machine that rejects illegal transitions. A webhook handler should look up the observation, not create a new credit.

Reconciliation is a scheduled job that compares your ledger against chain state for every payment in a non-terminal state older than a threshold. The output is a table you review, not an automatic mutation. A useful reconciliation report has one row per payment with columns for gateway status, chain status, confirmation depth, and the delta between expected and observed value.

CREATE TABLE observations (
  chain_id      INTEGER NOT NULL,
  tx_hash       TEXT    NOT NULL,
  log_index     INTEGER NOT NULL,
  invoice_id    TEXT    NOT NULL,
  block_number  BIGINT  NOT NULL,
  block_hash    TEXT    NOT NULL,
  value_wei     NUMERIC NOT NULL,
  seen_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (chain_id, tx_hash, log_index)
);

INSERT INTO observations (chain_id, tx_hash, log_index, invoice_id, block_number, block_hash, value_wei)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (chain_id, tx_hash, log_index) DO NOTHING;

Results table procedure for your own measurements

Do not adopt confirmation depths from blog posts, including this one. Measure them. Run the following procedure for each asset you accept and record the results in a table with these columns: asset, chain, observation window, reorgs observed at depth N, reorgs observed at depth N+1, median time to depth N, and p95 time to depth N.

Procedure: (1) run a watcher that stores block hash per height for at least 30 days; (2) nightly, compare stored hashes against the canonical chain and count mismatches by depth; (3) compute time-to-depth from block timestamps; (4) choose the smallest depth where observed reorgs are zero across the window, then add a margin for the value tier. Publish the table internally and revisit it quarterly. The numbers are yours; the method is the deliverable.

Troubleshooting common gateway failures

Most incidents fall into a small set of patterns. Work through them in order, because a chain mismatch invalidates every downstream check.

If the gateway reports a payment your RPC cannot find, confirm the chain ID first, then the transaction hash format. If the payment exists but the recipient differs, check whether the gateway used a forwarding contract. If the amount is short, check whether the sender paid network fees out of the transferred amount on a chain where that is possible. If a payment is credited twice, inspect the webhook handler for a missing unique constraint rather than blaming the gateway.

  • Chain mismatch: compare eth_chainId on both sides.
  • Missing logs: confirm the token contract address and topic hash.
  • Stale watcher: check the stored last-processed block against the chain head.
  • Rate limiting: batch log queries by block range and back off on 429 responses.
  • Clock skew: compare gateway timestamps with block timestamps before assuming latency.

Limitations and tradeoffs

Unique addresses improve matching but increase key-management surface and on-chain deployment cost if you use forwarding contracts. Shared addresses reduce cost but push complexity into amount matching and memos. Polling is simpler and resumable but adds latency; subscriptions are faster but require a completeness sweep. Custodial conversion removes volatility but adds counterparty and regulatory exposure.

There is also a measurement caveat: your watcher's latency is bounded by your RPC provider's head lag, which you do not control and which varies by network load. Treat observed latency as a property of the whole path, not of the gateway alone. For endpoint-level reliability characteristics, consult RPC Assistant rather than assuming uniform behavior across providers.

Next steps

Start by instrumenting before you optimize: log every observation with block hash and policy version, then run the reconciliation job for two weeks and review the report. Once the baseline is stable, tune confirmation depth per asset using your own table, and only then consider latency improvements such as subscriptions.

For the infrastructure side, review how blockchain node hosting works and the RPC endpoints guide to align your watcher with a reliable data source. If you are choosing between hosted and self-run infrastructure, the API service overview and the networks list describe what is available per chain. For the concrete question of which endpoint to use for a given network, ask RPC Assistant; that decision is provider-specific and outside the scope of this methodology.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started