URI, Clipboard, QR: How a Crypto Address Breaks in Transit
A crypto address passes through three encoders before it reaches your wallet: a URI, a clipboard and a QR code. We measured how each one breaks it — and what to check before you send.
An address is not an object you hand over — it is a string that gets encoded, copied, rendered, photographed and decoded before any money moves. On a swap order page it leaves through exactly three doors: a copy button, a 200-pixel QR code, and selectable text. Each door damages the string differently, and the damage is invisible, because every damaged variant still looks like an address. We measured all three on the live site: what the clipboard actually keeps, what the QR actually encodes, and how nine address formats respond to twelve mutations. The stakes are plain. At 18:06 UTC today a 100 USDT-TRC20 order quoted 0.13937044 XMR, and a destination that loses one character does not produce a failed order — it produces a paid order into nowhere. The catalogue at the same minute listed 434 assets on 62 networks, six of which also need a memo. (XMR to USDT exit)
Three URI standards cover the chains we route, and each one carries more than an address — an amount and a unit, which is exactly where a URI breaks quietly. BIP-21 defines bitcoin:<address>?amount=<decimal BTC>&label=&message=. The amount is decimal BTC with up to eight places, never satoshi, and clients must ignore parameters they do not recognise rather than refuse the payment. EIP-681 defines ethereum:<address>@<chainId>/transfer?address=<recipient>&uint256=<atomic units>. There the amount is an integer in the token's smallest unit — 1 USDT is 1000000, not 1 — and the spec explicitly allows scientific notation such as value=2.014e18. Monero uses monero:<address>?tx_amount=<decimal XMR>&tx_payment_id=<64 hex>, and wallet2.cpp shows the parser pushing any parameter it does not know into an unknown_parameters list and returning success. That last detail is the failure mode in one line: a URI written with amount= instead of tx_amount= is not rejected, it is accepted with no amount, and the human types the number by hand. (XMR to USDT exit)
We ran the site's validator across twelve variants of nine address formats — 90 cells, all recorded. Every URI form was rejected on every chain, with a plain reason such as "not a Bitcoin address (expect bc1…, 1… or 3…)". For a paste field that is the correct call: a swap should never execute an amount that arrived inside a string it did not generate, because an amount in a URI is an instruction, not a destination. The cost is that the round trip is one-way. If a wallet or block explorer hands you bitcoin:bc1q…?amount=0.01, the field will not take it; strip the scheme and the query, keep the path. Our own QR goes the other direction and encodes the bare address only, so nothing you scan can carry a wrong amount — and nothing you scan carries the right one either.
Now the clipboard, which is the least trustworthy of the three and the most used. Our copy button calls navigator.clipboard.writeText and wraps it in try/catch that swallows the error, so on an insecure origin or after a permission denial nothing is copied and nothing is said. The validator trims the string before checking, which quietly rescues four very common artefacts: a trailing newline, a trailing space, a non-breaking space (U+00A0) on either side, and a byte-order mark (U+FEFF) in front. Those four all passed on all nine formats. Three others did not: a zero-width space (U+200B), a right-to-left mark (U+200F) and a soft hyphen (U+00AD), inserted mid-string or appended, were rejected everywhere — and they are precisely what survives a copy from a PDF, a chat client or a web page that justifies text. Case is the second trap: an all-caps bech32 address is legal and passes, a mixed-case one fails its checksum, an all-caps Monero address is rejected outright, and 0X with a capital X is not an EVM address.
The QR code deserves its own section because its geometry is doing more work than it looks. The order page renders react-qr-code at size 200 with no level prop, which means the library default: error correction L, roughly 7 percent of the code recoverable. The component draws modules only and sets the viewBox to the module count, so the code carries no quiet zone of its own; the white margin is the 16-pixel padding of the container. At 200 CSS pixels that margin is 2.32 modules for a 29-module code, 2.64 for 33, 2.96 for 37 and 3.28 for 41 — the specification asks for four. Module size, meanwhile, runs from 6.90 pixels down to 4.88. And 106 characters is not an arbitrary length: it is exactly the byte capacity of version 5 at level L, which is why a 95-character Monero address and a 106-character integrated or unified address both land on 37 modules, while one extra character, or raising the level to M, pushes them to 41.
We then decoded those codes rather than trusting the geometry. Rendering the exact 200-pixel layout at four times scale, degrading it and decoding with jsQR 1.4.0 over twenty randomised trials per cell gave a clean curve. Down to 116 pixels wide — half the rendered size — all twenty-four address-and-level combinations read back correctly every time, including after JPEG compression at quality 40, a 2-degree rotation and a subpixel shift. At 93 pixels with 0.8-pixel blur, JPEG 40 and a 3-degree rotation, the short codes held: 29 modules 20/20, 33 modules 19–20/20. The long ones did not: 37 modules dropped to 9/20 and 8/20, and at 41 modules to 2/20 and 0/20. Raising error correction from L to M made those long codes worse, not better, because the same 200 pixels had to hold four more modules. A white disc covering 18 percent of the width, placed off-centre, was survivable: 16–19/20 for short codes and 20/20 for the long ones, which have more redundant area to lose.
The last gap is structural rather than geometric. The QR encodes the deposit address and nothing else, because that is the only value passed to the component; the memo lives in a separate copy row underneath. Six of the 434 catalogue assets require one — XRP, XLM, HBAR, ATOM, STX and NEIRO-eth — and the shared-deposit pattern makes it worse: on chains where the pool routes by tag, several orders share one address, and a scanner that stops at the QR sends to the address with no tag at all. On XRP that is not a delayed order, it is a deposit the pool cannot attribute. Reading the code confirms the shape: the QR is rendered from depositAddress, the memo from depositExtraId, in two different elements, and only one of them is scannable.
So the practical drill is short. Copy the address, paste it, then read the first four and last four characters against the screen before sending — a habit that catches every truncation and every glyph substitution at a glance. Never paste a URI into a field that expects an address, and never accept an amount from a URI you did not build. If you scan a QR, treat the amount as zero and type it from the order page, matching all eight decimals: 0.13937044 is not 0.1394, and on a 100 USDT order that difference is about 0.3 percent of the payout. If the asset needs a memo, do not scan at all — copy the address and the memo separately, and check the memo twice, because it is the only field with no checksum. Finally, screenshotting a QR and sending it through a chat app is where the 93-pixel case becomes real: the image survives, the address does not always. (XMR to USDT exit)
None of this is exotic. Three encoders, three failure modes, and a human in the middle who has no way to see any of them: a URI that silently drops the amount, a clipboard that carries a character with no width, a QR that is 37 modules wide in a 200-pixel box. The fix is not better software alone — it is knowing which door the string came through, so you can check the thing that door can break. Send from a wallet you control, keep the refund address on the same chain, and if you only remember one rule, make it this one: the address on the screen and the address in the wallet are two different objects until you have compared them. (recovering misdirected funds)