Docs
GuidesChains, Currencies & Confirmations

Chains, Currencies & Confirmations

Beginner10 min

Which networks and assets Havala settles, how many confirmations each needs, and how amounts are represented on the wire.


Havala settles on eleven chains. Seven are EVM and behave the same way as each other; four are not, and each is its own thing. Which one a payment runs on decides the deposit address format, the number of confirmations before the money counts, and how long that takes in wall-clock time.

Everything on this page is read from the chain registry the API uses at payment creation time, so a value here is the value your payment row will carry.

The eleven chains

EVM โ€” Ethereum, Polygon, BNB Smart Chain, Arbitrum One, Optimism, Base, Avalanche C-Chain. Same address format (0xโ€ฆ, 40 hex characters), same signature scheme, same tooling. An address is valid on all seven, which is exactly why a one-time deposit address is leased per payment: it is the only thing that disambiguates which invoice a transfer belongs to.

Non-EVM โ€” Bitcoin, TRON, Solana, TON. Different address formats, different finality models, different block times by three orders of magnitude.

The chain is named by a lowercase slug everywhere it appears in a request or a payment row:

ethereum, polygon, bsc, arbitrum, optimism, base, avalanche, bitcoin, tron, solana, ton

The uppercase PaymentBlockchainType enum (ETHEREUM, POLYGON, โ€ฆ) exists in the schema, but the payments.blockchain column is a plain VARCHAR carrying the slug. A ?blockchain= filter that sends ETHEREUM matches nothing. The one place the uppercase names are used is the merchant-level blockchain settings document, which is stored as free-form JSON.

Tokens and decimals

ChainNativeTokensDecimals
ethereumETHETH, USDT, USDC18, 6, 6
polygonPOLPOL, USDT, USDC18, 6, 6
bscBNBBNB, USDT, USDC18, 18, 18
arbitrumETHETH, USDT, USDC18, 6, 6
optimismETHETH, USDT, USDC18, 6, 6
baseETHETH, USDC18, 6
avalancheAVAXAVAX, USDT, USDC18, 6, 6
bitcoinBTCBTC8
tronTRXTRX, USDT6, 6
solanaSOLSOL, USDC9, 6
tonTONTON, USDT9, 6

USDT and USDC on BNB Smart Chain have 18 decimals, not the 6 they have everywhere else. Any code that hardcodes "stablecoins are 6 decimals" is off by a factor of a trillion on BSC. Read decimals off the checkout response rather than assuming:

TypeScript
// decimals comes back on the public checkout payment object. Use it, and use
// BigInt for the base-unit conversion โ€” Number loses precision above 2^53,
// which an 18-decimal token crosses at about 9 units.
function toBaseUnits(amount: string, decimals: number): bigint {
  const [whole, frac = ''] = amount.split('.');
  if (frac.length > decimals) throw new Error(`${amount} has more precision than ${decimals}`);
  return BigInt(whole + frac.padEnd(decimals, '0'));
}

toBaseUnits('149.5', 6);  // 149500000n        โ€” USDC on Ethereum
toBaseUnits('149.5', 18); // 149500000000000000000n โ€” USDT on BSC

Sending a token that a chain does not carry โ€” USDT on Base, SOL on Ethereum โ€” is rejected at checkout with INVALID_CURRENCY.

Confirmations

requiredConfirmations is not one global number. It is resolved per chain, and separately for mainnet and testnet, when the payment row is created โ€” and it is then frozen on that row. Compare a payment's confirmations against the requiredConfirmations the payment itself reports, never against a table you keep in your own code.

ChainMainnetTestnetBlock timeโ‰ˆ time to settle on mainnet
ethereum12612 s~2.5 min
polygon128322 s~4 min
bsc1563 s~45 s
arbitrum64120.25 s~15 s
optimism64122 s~2 min
base64122 s~2 min
avalanche1262 s~25 s
bitcoin3110 min~30 min
tron1963 s~1 min
solana32120.4 s~15 s
ton1265 s~1 min

The settle column is arithmetic โ€” required confirmations times block time โ€” not a guarantee. Real chains reorganise, congest and stall.

Two defaults sit behind that table and are worth knowing because they show up in odd rows. If the chain slug is not in the registry at all, the lookup falls back to 12. And the payments.requiredConfirmations column has a database default of 10, which the invoice-checkout path always overwrites with the per-chain value โ€” a row still carrying 10 was not created through checkout.

The confirmation tracker re-checks on a delay derived from the chain's block time, with a five-second floor. That is why a Bitcoin payment legitimately sits in CONFIRMING for half an hour while an Arbitrum one clears while the buyer is still looking at the page. Set the buyer's expectation from the chain they chose.

A merchant can raise the bar from the merchant dashboard, in the blockchain settings. That group carries a confirmationOverrides map โ€” chain slug to a confirmation count โ€” and it is written together with the enabled blockchains and enabled currencies rather than one field at a time. The map is stored verbatim and is not checked against the chain registry, so a mistyped slug is accepted and then silently never matches. Keep it to chains you actually use, and remember that every extra confirmation is block time the buyer waits through.

How amounts are represented

There are two amount representations in the API and they are not the same thing.

Invoice amount is a BigInt of minor units in the invoice's fiat currency, carried as a JSON string. "12500" with "currency": "USD" is $125.00. It is a string because a BigInt does not survive JSON.parse as a number safely, not because it is a decimal.

Payment amount, expectedAmount and receivedAmount are VARCHAR(80) decimal strings in the coin's own unit โ€” "0.04217", "149.500000". They are strings so that an 18-decimal value survives the round trip without a float's rounding. Parse them with a decimal type. receivedAmount starts at "0" and stays there until a deposit is observed.

One quirk to hold onto: on the invoice-checkout path the payment is opened with paymentAmount and expectedAmount copied verbatim from the invoice's fiat minor units, because fiat-to-crypto conversion is not applied at that step. Read amount alongside cryptoCurrency and decimals rather than assuming a conversion happened, and do your own received-versus-expected comparison in the webhook handler.

Testnets

Each chain has one testnet in the registry:

ChainTestnetChain id
ethereumSepolia11155111
polygonAmoy80002
bscBSC Testnet97
arbitrumArbitrum Sepolia421614
optimismOP Sepolia11155420
baseBase Sepolia84532
avalancheFuji43113
bitcointestnetโ€”
tronNileโ€”
solanadevnetโ€”
tontestnetโ€”

Mainnet chain ids, for the EVM seven: Ethereum 1, Optimism 10, BNB Smart Chain 56, Polygon 137, Base 8453, Arbitrum One 42161, Avalanche C-Chain 43114.

The payment row records which network it ran on in a NetworkEnum column:

MAINNET, SEPOLIA, GOERLI, HOLESKY, AMOY, MUMBAI, BSC_TESTNET (stored as bsc-testnet), FUJI, NILE, TESTNET, DEVNET, TESTNET_GENERIC (stored as testnet-generic).

The network field on POST /api/v1/invoices/{id}/checkout is written straight into that column. It has one sharp edge:

Omit network entirely for mainnet. The isTestnet flag is derived as "was anything other than the lowercase string mainnet supplied", so passing the uppercase "MAINNET" marks the payment as testnet โ€” a live-looking payment carrying testnet confirmation counts. And passing the lowercase "mainnet" is not a valid enum value, so that write fails outright.

For a testnet, pass the exact uppercase enum name (SEPOLIA, AMOY, BSC_TESTNET, โ€ฆ). See Testing on Testnets.

Smart-contract payments

The seven EVM chains also support a contract path, where the buyer's wallet calls a Havala payment contract instead of transferring to a leased address. The payment is paymentType: SMART_CONTRACT and carries contractAddress, externalId, treasuryWallet, tokenAddress (null for the chain's native coin) and chainId instead of a paymentAddress.

Bitcoin, TRON, Solana and TON are direct-transfer only; asking for the contract path there fails with BLOCKCHAIN_NOT_SUPPORTED. The mechanics are in The Hosted Checkout Page.

NextWhat it covers
Accepting Crypto PaymentsChoosing a chain at checkout and watching it confirm
Testing on TestnetsFaucets and a scripted end-to-end run
The Hosted Checkout PageLetting the buyer pick from this matrix