BIP-21, EIP-681, Solana Pay: crypto payment URIs and their traps
2026-07-31
Three standards, three unit conventions
A crypto payment QR code does not hold an address: it holds a URI. Bitcoin, Ethereum and Solana each standardised their own — BIP-21, EIP-681, Solana Pay — and the three diverge exactly where a mistake is most expensive: the unit the amount is expressed in.
This is not an implementation detail. A generator that treats all three the same way produces QR codes requesting the wrong amount, with no error anywhere to signal it. The wallet shows a number, the user approves it, and the transaction goes through.
BIP-21: the oldest, the most readable
The Bitcoin scheme looks like an ordinary URL:
bitcoin:36hvVTFfk7WJD9LFmVmnELkvQtcvSTpgEx?amount=0.01&label=Cafe&message=Table%204
The amount parameter is expressed in BTC, in plain decimal notation. 0.01 means one hundredth of a bitcoin, which reads naturally. label identifies the payee, message describes the reason — both optional and purely informational.
BIP-21 also accepts a lightning parameter carrying a BOLT-11 invoice in the same URI. The wallet then picks the route it can handle: on-chain if it has no Lightning support, off-chain otherwise. One QR code covers both.
EIP-681: the wei trap
On Ethereum the basic form looks similar:
ethereum:0x75D7b06d710B47deE8E15Db392e0e8B20345a211@137?value=10000000000000000
The @137 is the chainId defined by EIP-155 — 1 for Ethereum, 137 for Polygon, 56 for BNB Smart Chain, 43114 for Avalanche. It prevents sending funds on the wrong network, a common accident when the same address exists on several chains.
But value does not read like amount. EIP-681 requires an integer in wei, Ethereum's atomic unit, or 10⁻¹⁸ ETH. The amount above is 0.01 ETH, written 10000000000000000.
The classic mistake is to copy the user's input straight through. A QR announcing value=0.01 does not request 0.01 ETH: it requests 0.01 wei, an amount so small it rounds to zero. The payment succeeds, the merchant receives nothing, and nothing in the flow flagged the problem.
The standard's ABNF also allows scientific notation — value=1e16 — which is shorter. Wallet support for it is uneven, whereas the full decimal integer is understood everywhere, and the extra eighteen characters have no measurable effect on QR density. On a payment path, compatibility beats brevity.
Why floating-point multiplication is disqualified
Converting to atomic units looks trivial: multiply by 10¹⁸. It is not, because IEEE-754 floating-point numbers do not represent decimals exactly.
Measured in Node:
1.1 * 1e18yields 1100000000000000100 instead of 11000000000000000000.07 * 1e18yields 70000000000000010 instead of 70000000000000000
A hundred wei of drift on an ETH payment changes nothing in practice. But the same code applied to a 6-decimal token shifts cents, and on a payment path a silent rounding error is never acceptable.
The fix is to never go through a number: shift the decimal point on the string. Pad the fractional part with zeros to the required length, concatenate, strip leading zeros. The result is exact by construction, whatever the value.
Transferring a token: the target flips
Sending USDC rather than ETH changes the structure of the URI, and this is the part most generators get wrong:
ethereum:0xCONTRACT@137/transfer?address=0xRECIPIENT&uint256=12500000
Three differences, each a source of error:
- The URI targets the token contract, not the recipient, who moves into the
addressparameter. Swapping the two sends the funds to the contract itself, where they are unrecoverable. - The amount is called
uint256, after the Solidity type thetransferfunction expects. - There is no
value. The native asset does not move; only the token is transferred. A leftovervaluewould send ETH on top of the token.
Token decimals: the 10¹² mistake
uint256 is expressed in the atomic unit of the token, and every token defines its own decimals in its contract. USDC and USDT use 6. DAI, WETH and most others use 18. WBTC uses 8.
That variability is what makes the mistake so treacherous: unlike wei, the displayed amount stays a plausible number. The error runs both ways, with a factor of 10¹² each time:
- Declaring 18 decimals on a 6-decimal token requests 12,500,000,000,000 USDC instead of 12.50. That is the lucky case: the transaction fails for lack of balance.
- Declaring 6 decimals on an 18-decimal token requests 0.0000000000125 DAI instead of 12.50. And that one goes through.
There is no way to guess the decimals offline: you read them from the contract, or you know them. This is why web3qr asks for the value explicitly rather than assuming one, and ships no hardcoded contract address — a wrong token address in the code would send funds to the wrong contract, with no local check able to catch it.
Solana Pay: a third convention
Solana takes a middle position:
solana:B38he25bGWrwuvAYa3dPDKVkXGzSYftxqoC8sitkGg1r?amount=12.5&spl-token=EPjFWdd5...
The amount parameter stays decimal, like BIP-21 — the specification expresses it in user units. More importantly, adding spl-token does not change the target: the URI still points at the recipient, never at the mint. The wallet reads the token's decimals from the chain, which removes the entire class of error described above.
Solana Pay also defines reference, a public key acting as a unique identifier for locating the transaction, and memo, written into the ledger. A merchant can reconcile a payment with an order without any intermediate database.
What to remember about units
- BIP-21:
amountin BTC, decimal. - EIP-681 native:
valuein wei, integer, 18 decimals. - EIP-681 token:
uint256in the token's atomic unit, integer, variable decimals. - Solana Pay:
amountin user units, decimal, for both SOL and SPL tokens.
Validate the address, not just its shape
Many generators check addresses with a regular expression: right length, right alphabet, right prefix. That is not enough, because all three properties survive a typo.
Every address format embeds a checksum, precisely to catch this:
- Base58Check for legacy Bitcoin addresses — four bytes of double SHA-256.
- Bech32 and Bech32m for SegWit, defined by BIP-173 and BIP-350.
- EIP-55 for EVM addresses: the mixed case encodes a keccak256 of the lowercase address.
- Solana public keys are ed25519 points in Base58, whose decoding fails if a character changes.
web3qr verifies these checksums locally, in the browser, before generating the QR code. An address with one altered character is rejected as you type — not after a thousand posters have been printed.