VeilSwap
← All posts
Guides2026-11-039 min read

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)

Frequently asked questions

What are the three ways an address can break between devices?

URI formats, the clipboard and QR codes each damage a different part of the string. A URI can carry the wrong unit or silently drop an amount; a clipboard can add invisible characters such as U+200B that our validator rejects on all nine formats tested; a QR code at 200 pixels loses decode reliability once the address exceeds roughly 80 characters and the image is scaled below half size. The address itself, the part you cannot afford to lose, is the one part all three channels preserve — which is why the check is always the same: compare the destination, and re-enter the amount.

Does VeilSwap accept a bitcoin: or monero: URI in the address field?

No. All URI-prefixed variants were rejected across nine address formats and twelve mutations in our test, with errors such as "not a Bitcoin address". Only the bare address is accepted, and the front end also trims surrounding whitespace before submitting. If a wallet gives you a URI, strip everything up to and including the scheme and the query string, keep the address, and type the amount yourself from the order page.

Why does raising the QR error-correction level make long addresses harder to scan?

Because the code has a fixed pixel budget. At the rendered size of 200 CSS pixels, moving a 95-character address from level L to level M grows the symbol from 37 to 41 modules, shrinking each module from 5.41 to 4.88 pixels. In our degraded-image trials that change cut successful decodes from 9/20 to 2/20. Error correction only helps once modules are large enough to sample; below that threshold, more redundancy is paid for with smaller dots, and the trade goes the wrong way.

Keep reading

All articles →

Ready to swap?

No account · No KYC · No email

Exchange now
URI, Clipboard, QR: How a Crypto Address Breaks in Transit