A cryptocurrency gateway is the software layer that watches a blockchain for incoming transfers, decides when a payment is final enough to credit, and reconciles that decision against your ledger. The hard parts are not the checkout button but confirmation policy, reorg handling, idempotent crediting, and the reliability of the node or RPC endpoint that feeds the watcher. This article explains the mechanism, gives runnable detection and reconciliation code, and shows how to measure the failure modes that matter before you route real money through a gateway.
What a cryptocurrency gateway actually does
A cryptocurrency gateway sits between a customer's on-chain transfer and your internal accounting. It does not move coins by itself; it observes the chain, matches transfers to orders, applies a confirmation policy, and emits a credit or settlement event to your application. In the card-payment analogy, the gateway is closer to the acquiring processor plus the reconciliation desk than to the checkout form.
That distinction matters for engineering. A gateway has three separable responsibilities: detection (finding candidate transactions), attribution (deciding which order a transaction belongs to), and finality (deciding when the payment is safe to credit). Each has its own failure modes, and each depends on a data source — usually a node or RPC provider — whose consistency you must verify rather than assume.
This guide covers self-hosted and provider-backed gateway designs. It does not recommend a specific payment vendor; for the node and endpoint layer underneath a gateway, use the OnFinality RPC Assistant for concrete provider choices and keep this page as the methodology reference.
- Detection: poll or subscribe for transactions touching your deposit addresses.
- Attribution: map amount, address, memo, or invoice ID to an order.
- Finality: apply confirmation depth and reorg rules before crediting.
- Reconciliation: compare on-chain state to your ledger on a schedule.
Assumptions and prerequisites
The procedures below assume you control the deposit addresses (or can derive them), you have an RPC endpoint for the target chain, and you maintain an internal ledger with idempotency keys. They also assume you can run a small worker process with persistent storage; a gateway that keeps state only in memory will lose track of pending payments on restart.
You need a way to query blocks and transactions (JSON-RPC eth_getBlockByNumber, eth_getLogs, or the chain's equivalent), a database table for observed transfers, and a job scheduler for confirmation checks. If you are still choosing infrastructure, the guide to blockchain node hosting covers the operational baseline, and RPC endpoint fundamentals explains the request patterns a watcher depends on.
The payment lifecycle and confirmation policy
A typical lifecycle is: customer selects crypto, your system issues an invoice with an address and expected amount, the customer broadcasts a transfer, your watcher detects it in a block, the transfer accumulates confirmations, and at a threshold your ledger credits the order. Settlement to fiat or stablecoins, if you need it, happens after crediting and is a separate concern.
Confirmation depth is a risk decision, not a constant. It should depend on the chain's reorg behavior, the payment size, and your tolerance for reversal. A practical policy is tiered: small amounts credit at a shallow depth, larger amounts at a deeper depth, and anything above a treasury threshold waits for a manual review. Document the policy and encode it in configuration so it can be audited.
A results table you should produce for your own chain and amounts (values below are placeholders, not measurements):
| Payment tier | Required confirmations | Expected wait | Reorg exposure | Action on reorg |
|---|---|---|---|---|
| Small | TBD | TBD | TBD | Hold and re-check |
| Medium | TBD | TBD | TBD | Freeze credit, alert |
| Large | TBD | TBD | TBD | Manual review |
Fill the TBD cells by measuring your chain's observed reorg depth over a sample period and by timing confirmation latency against your RPC provider. Do not copy another chain's numbers.
Detecting transfers with a polling worker
Polling is the simplest reliable detection method and the easiest to reason about during incidents. The worker advances a cursor block by block, fetches each block, and records transfers to your addresses. The example below uses JSON-RPC over HTTP; replace RPC_URL with your endpoint and keep the cursor in durable storage.
Run it against a testnet first and verify that the cursor advances exactly once per block and that restarts resume from the stored cursor rather than from the chain head.
import json, time, urllib.request
RPC_URL = "RPC_URL"
DEPOSIT_ADDRESSES = {"0xYourDepositAddress"}
CURSOR_FILE = "cursor.json"
def rpc(method, params):
body = json.dumps({"jsonrpc":"2.0","id":1,"method":method,"params":params}).encode()
req = urllib.request.Request(RPC_URL, data=body, headers={"Content-Type":"application/json"})
with urllib.request.urlopen(req, timeout=15) as r:
return json.loads(r.read())["result"]
def load_cursor():
try:
return json.load(open(CURSOR_FILE))["block"]
except FileNotFoundError:
return int(rpc("eth_blockNumber", []), 16) - 1
def save_cursor(n):
json.dump({"block": n}, open(CURSOR_FILE, "w"))
cursor = load_cursor()
while True:
head = int(rpc("eth_blockNumber", []), 16)
while cursor < head:
cursor += 1
block = rpc("eth_getBlockByNumber", [hex(cursor), True])
for tx in block["transactions"]:
to = (tx.get("to") or "").lower()
if to in {a.lower() for a in DEPOSIT_ADDRESSES}:
print("candidate", tx["hash"], tx["value"], cursor)
save_cursor(cursor)
time.sleep(5)Attribution: matching transfers to orders
Detection without attribution produces a stream of transfers you cannot credit. Attribution strategies fall into three families: unique deposit address per order, a shared address plus a memo or invoice ID, and amount-plus-time matching. Unique addresses are the most robust because the address itself is the key; memos work only on chains that carry them reliably; amount matching is fragile under load and should be a fallback, not the primary method.
Whichever you choose, store the mapping before you show the address to the customer, and make crediting idempotent on the transaction hash. A duplicate webhook or a replayed block must not credit twice.
- Unique address per invoice: simplest attribution, more address management.
- Shared address with memo: fewer addresses, depends on wallet support.
- Amount and time window: weakest, use only for low-value flows.
Confirmations, reorgs, and idempotent crediting
A transfer seen at block N can disappear if the chain reorganizes. Your gateway must treat detection and crediting as separate states: observed, confirmed, credited, and reversed. When a reorg removes a previously observed transaction, move it to reversed and reverse the ledger entry only if it was already credited — and only through an auditable adjustment, never a silent delete.
The check below queries the current block and compares it to the block where the transfer was seen. It is deliberately simple; production systems should also verify the transaction is still included in the canonical chain by re-fetching the receipt.
async function confirmationStatus(rpcUrl, txHash, seenAtBlock, required) {
const call = async (method, params) => {
const res = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
});
return (await res.json()).result;
};
const receipt = await call("eth_getTransactionReceipt", [txHash]);
if (!receipt) return { state: "reversed" };
const head = parseInt(await call("eth_blockNumber", []), 16);
const depth = head - parseInt(receipt.blockNumber, 16);
return { state: depth >= required ? "confirmed" : "observed", depth };
}Measuring RPC reliability before trusting a gateway
Every gateway is only as consistent as the endpoint feeding it. Before routing real payments, run a measurement pass against your candidate RPC endpoint and record the results in a table you own. The procedure: send a fixed sequence of eth_blockNumber and eth_getBlockByNumber calls at a steady rate for a defined window, log latency and error codes, and repeat against a second endpoint for comparison.
Record at minimum: request count, success rate, p50 and p95 latency, rate-limit responses, and any block-height disagreements between endpoints. A gateway that sees stale block heights will under-detect payments; one that sees inconsistent heights across providers will produce false reorg alarms. If you need a managed endpoint, the RPC API service and RPC pricing pages describe the commercial options, and dedicated nodes are the right choice when you need predictable throughput for a high-volume watcher.
Do not publish or rely on numbers you did not measure. The table below is the shape of the result, not a result:
| Endpoint | Requests | Success % | p50 ms | p95 ms | Rate-limit errors | Height disagreements |
|---|---|---|---|---|---|---|
| Endpoint A | TBD | TBD | TBD | TBD | TBD | TBD |
| Endpoint B | TBD | TBD | TBD | TBD | TBD | TBD |
Reconciliation and expected verification output
Reconciliation compares your ledger to on-chain state on a schedule. For each credited order, re-fetch the transaction receipt and confirm the recipient, amount, and confirmation depth still match. The expected output of a healthy run is a report with zero unexplained differences: every credited order maps to a canonical transaction, every observed-but-uncredited transfer has a reason, and every reversal has a matching ledger adjustment.
A minimal reconciliation query returns the set of credited orders whose transaction is no longer canonical. If that set is non-empty, your gateway has a correctness bug or a reorg policy gap; investigate before the next settlement cycle.
SELECT o.id, o.tx_hash, o.credited_at
FROM orders o
LEFT JOIN canonical_txs c ON c.tx_hash = o.tx_hash
WHERE o.status = 'credited' AND c.tx_hash IS NULL;Troubleshooting common gateway failures
Most gateway incidents fall into a small set of patterns. Missing payments usually mean the watcher cursor stalled, the deposit address was mistyped, or the RPC endpoint returned stale blocks. Duplicate credits usually mean the idempotency key was not enforced at the database level. False reorg alarms usually mean two endpoints disagree on block height and the gateway trusted the wrong one.
When debugging, freeze crediting first, then inspect the cursor, the observed-transfer table, and the endpoint's reported block height. Compare against a second independent endpoint before concluding that a reorg occurred. For deeper endpoint diagnostics, see the multi-chain RPC guide and the network directory for chain-specific behavior.
- Cursor stalled: check worker logs and stored cursor value.
- Stale blocks: compare head height across two endpoints.
- Duplicate credit: verify unique constraint on transaction hash.
- Reorg alarm: confirm the transaction is absent from the canonical chain.
Limitations and tradeoffs
Self-hosted gateways give you control over confirmation policy and data residency but require you to operate nodes, handle chain upgrades, and build reconciliation yourself. Provider-backed gateways reduce operational load but move finality policy and fee handling into a third party, which may conflict with your compliance or treasury requirements.
Polling is simple but adds latency equal to your poll interval; subscriptions reduce latency but add reconnect complexity. Unique deposit addresses improve attribution but increase address derivation and monitoring overhead. There is no universally correct configuration — the right choice depends on payment sizes, chain behavior, and how much operational risk you can absorb.
Next steps for a production gateway
Start by writing down your confirmation policy per chain and per payment tier, then implement detection, attribution, and crediting as separate services with idempotent boundaries. Run the reliability measurement against at least two endpoints, record the results in your own table, and set alert thresholds on success rate and block-height disagreement.
Once the pipeline is stable on a testnet, rehearse a reorg and a provider outage deliberately. If you need help selecting the endpoint layer, the RPC Assistant can recommend concrete providers, while the RPC endpoint guide and node hosting guide cover the operational details.