Skip to content

Wallet URI cookbook

Recipes for the crypto-wallet use case: turn a payment request or a pairing URI into a QR code you can show in a terminal and scan with a phone wallet. For the underlying URI formats see the BIP-21, Lightning, and EIP-681 summaries; for the rendering model and CLI see the README.

Each builder returns a plain str, so it composes with the rest of cuere — optimize_uri to shrink the code and show / render to draw it. The codes below are the real render output: plain Unicode half-blocks, the same shapes show prints to a terminal (a dark module is foreground ink, so in a light-on-dark terminal you may want --invert for a scanner). The addresses are valid, but some invoices and keys are shortened for a compact illustration.

Why error-correction level L?

cuere encodes at QR error-correction level L (~7% recovery), the lowest of the four levels, with boost_error=False — and that is deliberate, not a default to "fix". A terminal is a clean, backlit, high-contrast surface that does not smudge, fade, or pick up a coffee ring like a printed sticker, so the redundancy that protects a printed code buys almost nothing on screen. Level L spends the fewest modules on recovery, which means a smaller code: fewer modules per side, larger on-screen modules, and an easier scan. If you export a code to print it (see the exporting recipe), pass a stronger level explicitly — error="M", "Q", or "H" — where physical wear actually matters.

Bitcoin payment request (BIP-21)

bitcoin_uri builds a well-formed BIP-21 bitcoin: URI: the address is validated, amount is rendered as a plain BTC decimal, and label / message are percent-encoded.

from decimal import Decimal

from cuere import bitcoin_uri, show

uri = bitcoin_uri(
    "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
    amount=Decimal("0.005"),
    label="Coffee",
    message="Order #1234",
)
# bitcoin:bc1q...?amount=0.005&label=Coffee&message=Order%20%231234

show(uri)   # draw it in the terminal
    █▀▀▀▀▀█ ▄█ ▄▄▄▀▄█▀▀▀█ █▀█▄▀▄█ █▀▀▀▀▀█    
    █ ███ █ ▄█▀█ ▄▄ ▀▄█ ▀▀█  ▄▄█  █ ███ █    
    █ ▀▀▀ █ ▄ █ ▀ ▄██▀ ▀▀█ ▀█▄█▄▀ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀ ▀ █ ▀▄█ ▀▄▀▄█ ▀ ▀ ▀ ▀▀▀▀▀▀▀    
    ▀▀▀▀▀ ▀██▀▀ ▀▀▀ ▀ ▄▄▄ ▀▄ ▀▀ ██ █ █▄▀     
    █  ▀▀ ▀▄▀▀▄█▀ ███▀▀ ▀██▄▄ ▄▀▀▄█▄▀  █▀    
       ▀█▀▀█ ▄█▀█▀█▀▄█▄▄▄ ▀▄▄▀█▀▀▀▀▀▀▀█▀     
    █▄  ██▀  ▀▄ █▀ ▀█▀  ▄▄█▄▀▀ ▀ ██▄ █▀█▀    
     ▄▀ ▀█▀▄▄ ▀ █▀ ▀▄▀▄█▄▄█▄▀██▀█▀▀▄▀▄█ ▀    
     ▀██▀▀▀▄ ██ ▀ ██▀▀▄▄▄██ █ ▄██   ▄▀▀▀█    
    █▀█ ▀▄▀▀▀▀ ▄▄▀▄▀▄▀▄▄  ▀█▄▀▄▀▀█▀▀▀█▀█▀    
    ▀   ▀▄▀█▀ █▄▄▀ ▀▀▀▄ █▄▀▄▀  █▀ ▄  ▀▀██    
    █▄█▄▀█▀  █▀▄▄▀▄▀█▀▄▄▀▄█▄ ▀▀ ██▀▀▀█▀      
    █ ▄▀▀█▀▄ ▀ ▄█  ▀▀    █▀▄▄▀▄█▄▀██ ▄▀▀▀    
    ▀  ▀ ▀▀▀▄  ▄▄▀ ▀█▀▄▄▀ ▄█▀▀▄▀█▀▀▀█▄▀ ▀    
    █▀▀▀▀▀█ ▀ ▀ ▄ ███    ██▄  █▄█ ▀ █  ▀█    
    █ ███ █ █▄ ▄▄▀▄▀▄▀█▄▀▄▄▄▄██▀█████▄▀▀▀    
    █ ▀▀▀ █ █ ▄▄▄▀ ██▀▄▄▄█ ▄   ▄▄▄▄▄▄  ▀█    
    ▀▀▀▀▀▀▀ ▀▀   ▀   ▀ ▀     ▀▀   ▀▀  ▀▀▀    

amount is in BTC and accepts Decimal, int, or str — never float, which can't represent decimal money exactly. It must be a finite, positive value no finer than one satoshi (8 decimal places); anything else raises WalletURIError:

from cuere import WalletURIError

addr = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"

bitcoin_uri(addr, amount="0.00000001")   # ok: exactly one satoshi
bitcoin_uri(addr, amount="0.000000001")  # WalletURIError: sub-satoshi (9 dp)
bitcoin_uri("bad address!")              # WalletURIError: bad address

For the underlying format see the BIP-21 summary.

Lightning invoice (BOLT11 / LNURL / BOLT12)

lightning_uri wraps a bech32 Lightning payload — a BOLT11 invoice (lnbc…), an LNURL (lnurl1…), or a BOLT12 offer (lno1…) — in a lightning: URI. Like bitcoin_uri it validates the payload structurally (ASCII-alphanumeric, a single case) but does not verify its checksum:

from cuere import lightning_uri, show

uri = lightning_uri("lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqf")
# lightning:lnbc1pvjluez...

show(uri)   # draw it in the terminal
    █▀▀▀▀▀█ ▀▀▀█▄███ ▀▄▄ ▀    █▀▀▀▀▀█    
    █ ███ █  ▀▄█ ▀█▄▄▄ ▀ █  ▀ █ ███ █    
    █ ▀▀▀ █  ▄▀█▀█▄▀▄▀▄▀▄██▄  █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █▄▀ █ █ ▀ ▀▄█ █ █ ▀▀▀▀▀▀▀    
    ██ █▀ ▀▄▄▀▄ ▀▀█▀██ █▀▄ ▄ ▄▀▄▄▄▄▄▀    
    ▄█▀  █▀█▀▀  ▀ ██ █▀▄█▀ █  ▀ ▄▀█▀▀    
     ▀▀  █▀█▄██ ▄▀▀██  ▀▀ ▄█▄ ▀  █ █     
     ▀█ ▀▄▀█▀█▄ ▄▄█▀▄ ▀▀█▀▀ █   ▄▄▄▄     
    ▀▀█▀▀ ▀▄▀█ █▀  █▀▄▄▄▄ █▀ ▀████ ▀█    
    ▀█ ▀  ▀ ▄██▄▀█▀█▀ ▀▀█ █▀██ ▀ ▄█ █    
    █    ▄▀█▀█▄▀▄█ ▄ █ ▄▀▄ ▄   █▄▄ ▀▀    
    █ ▀ ▀▀▀▀▀▄▄█▄█ █▀█▀▄▄▀ ▀█▀ █▀▀▄▀▀    
    ▀▀ ▀  ▀▀▄▄▄▀▀█▄█▄  ▀  ▄▄█▀▀▀██▄▄     
    █▀▀▀▀▀█  ▀ █▀▀ ▀▄ ▀▀█▀▀██ ▀ █▀▄▄▄    
    █ ███ █ ██ ▄▄▄██▀▄▀▄▄  ▀██▀▀▀▄ ▄▄    
    █ ▀▀▀ █ ▄█ ▀▄ ▄█▀▀ ▀█▀▄ ▀ ▄ ▀█▄██    
    ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀▀  ▀▀            

A lightning: URI is bech32 and so case-insensitive, just like a bare bitcoin: address — pass a lowercase one through optimize_uri to uppercase it into QR alphanumeric mode for a smaller, faster-to-scan code (see Shrinking codes below). Lightning addresses (user@domain) are not bech32 and are out of scope. For the formats see the Lightning summary.

Ethereum payment request (EIP-681)

ethereum_uri builds an EIP-681 native-payment URI. The value is in wei (1 ETH = 10¹⁸ wei) — cuere matches the spec rather than hiding a unit conversion, so convert from ether yourself:

from cuere import ethereum_uri, show

ONE_ETH = 10**18
uri = ethereum_uri(
    "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359",
    value=ONE_ETH // 100,   # 0.01 ETH, expressed in wei
    chain_id=1,             # EIP-155 mainnet
)
# ethereum:0xfb69...d359@1?value=10000000000000000

show(uri)   # draw it in the terminal
    █▀▀▀▀▀█ ▄█▀▀█▀█▄▄▄▄▀▄▀ ▀▄ █▀▀▀▀▀█    
    █ ███ █ ▄▀  ▀▄▀█▀▄▄▀ ▀▄ ▀ █ ███ █    
    █ ▀▀▀ █ ▄█▀▀█ ▀▄█▄ ▀▀▀ ▀█ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀▄▀ █ █▄█ ▀ █▄▀▄▀ ▀▀▀▀▀▀▀    
    █▀█▀▀ ▀███ ▄▄▀▄▀█▀▄▄▀▄▄▄▄█▄█ ▀▄█▄    
    █▀ ▀█▀▀  █  █  █▄▀▀▄█▄█▄▀▄ ▄▄▄▄█     
    ▀▄▄█  ▀ ▀▀ ▄▄▀▄ ▄▀▀▄▀ ▄▄▀▄▄▄▀▄▄▀▄    
    █▀█▀ █▀▄▀▄▄ █  █▀   ▀█▄ ▀▀▄▀▀██▀     
    ▀▄ █▄ ▀▄█ ▄▄▄▀▄▀▄▀▄▄▄█▀▄▀▄▄▄▀█▄ █    
    ▄██ ▀▀▀▀ ██ █  ▀▄ ▄ █▄▄ ▄  █▀██▀     
    ██  ▀█▀ ▄ ▀▄▄▀▄▀█▀▀▄▄▄▀▄▄▄▄▀▀▄ █▄    
    █   ▄▀▀▄▀▀▄ █  ▀█▀▀▄▀██ ▄▄▀▀▀▀██     
    ▀  ▀▀▀▀ ▄▄▄▄▄▀▄ ▀▀▀▄▀ ▄ █▀▀▀█▀ ▀▄    
    █▀▀▀▀▀█ ▀   █  █▀   ██▄██ ▀ █ ▄█     
    █ ███ █ █▀▀▄▄▀▄▀▀█▀▄▄▄▄▄▀▀▀██▀▄▀▀    
    █ ▀▀▀ █ █ ▀ █  █  ▀▄▀▄  ▀█▀▄ ▄█▀     
    ▀▀▀▀▀▀▀ ▀▀▀  ▀ ▀      ▀▀▀▀ ▀▀▀ ▀     

The address case is significant — when mixed-case it carries the EIP-55 checksum — so an ethereum: URI is never passed through optimize_uri (it would corrupt the checksum). value, gas_price, and gas_limit are non-negative uint256 integers; invalid input raises WalletURIError.

ERC-20 token transfer

erc20_transfer_uri builds the EIP-681 transfer(address,uint256) form. amount is in the token's base units (its own decimals, which the library can't know — so apply any scaling yourself):

from cuere import erc20_transfer_uri, show

# 1 USDC (6 decimals) to the recipient, on Ethereum mainnet.
uri = erc20_transfer_uri(
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",  # USDC contract
    to="0x8e23ee67d1332ad560396262c48ffbb01f93d052",
    amount=1_000_000,                               # 1 USDC = 10**6 base units
    chain_id=1,
)
# ethereum:0xA0b8...eB48@1/transfer?address=0x8e23...d052&uint256=1000000

show(uri)
    █▀▀▀▀▀█ ▄▄▀██ ▀▄▀▀█ ██   ▄█▄▄▄▄▀▄ █▀▀▀▀▀█    
    █ ███ █ ▄▀█▀▄▄  █▄  ▀▀█▄▀▄ ▄▄▄▀▄▄ █ ███ █    
    █ ▀▀▀ █ ▄█ █ ▄▀▄ ▄█▀██▄▀█ ▀▄  █▀▄ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀▄▀ ▀ █▄▀ █ █▄█ █ █ ▀ ▀▄▀ ▀▀▀▀▀▀▀    
    ██▀▀▀ ▀▀█▀▀▀▄▀▀▀▄█▀▄▄▄▀▄▄▀▀▀▄▀▄▄▀▀▄█▄▀▄▀▄    
    ▀ ▀▀▄▄▀▄▀▀▄██  █▀▀▄▄▄▄▀  ▀ ▀▄ ▀ ▀   █▄ ▀▄    
     █▄   ▀██▀ ▀▄▀▄▀▀▀ ▄█▄▄▄▄▀▀▀▀▀▄█▀▄▄▄▄▄██▄    
    ▀▄▀▀█▄▀  ▄  ▄▀▀█  ▄▄▄▄ ▄█ ▀▀▄▀▀▄▀█▀▀▄▀ ▄     
    █ █▀█▄▀ ▀ █▄ ▀▀▀▄▀▀▄▀ ▄▄▀▀▄▀▀▀▀▄▀▄ ▀▄█▀ ▄    
     ▀ ▄ ▄▀█▄ ▄ ▀  ▀  ▄  █▄▄█▀▄▀▀ ▄▄▄    █  ▄    
    ▀ ▀▄▀█▀█▄▄▀ ▀▀▄▀▀█▀▄ ▄▄▄▀▀▄▀▀▀▀▄▀▀▄▀▄▀▀▀▄    
     ██ ▄█▀▀ ▄▀█▀ ██▀ █▄ █ ▄█ ▄█▀▀▄ ▀▀  ▄▄       
    ▀█▀▀▄ ▀▄█▀▀█▀▀█▀█ ▄▄▀▄▀▄▄▀▄ ▀▀▀▄▀▀▄▀▄▀▀ ▄    
     ▀▄█▄ ▀▀ ▀▄ ▀  █▄▀█▄▄▄▄ ▄ ▀▀     ▄▄█▀▄▀      
    ▄ ▀▀ ▄▀ █▀█ ▄  ▀▀▀▀▄▀▄▀▄▀█▄▀▀▀▀█▀▀▄▀▄ █▄▄    
    █ █▄ █▀▀▄   ▄▀ ██ █ █▄▄▄▀ ▄▀▀▀█ ▄█ ██        
    ▀  ▀▀▀▀ ▄▀ ▄▀▀▄ █▀▀▄▀█▀▄▀▄▀▀▄▀▀▄█▀▀▀██▀█▄    
    █▀▀▀▀▀█ ▀▄▄ ▄  ▀█  ▄▀█▀ ▀  █ ▀▄▀█ ▀ ██ ▄▄    
    █ ███ █ █ ▄ ▀▄  ▀▀▀▄▀▄▄▄▀▀▀ ▀▀▀ █▀▀▀▀▀██     
    █ ▀▀▀ █ █ ██▀▀█▀▄ ▀  ██▄▀ ▀█  ▀ ▄▀▀██▄ █▀    
    ▀▀▀▀▀▀▀ ▀ ▀▀▀▀▀▀ ▀▀ ▀    ▀ ▀▀▀▀▀  ▀▀▀ ▀      

See the EIP-681 summary for the full grammar, the wei/base-unit conventions, and the EIP-55 case-significance rule.

WalletConnect pairing (wc:)

A WalletConnect wc: URI is the pairing handshake a dapp shows so a wallet can connect to it. cuere has no walletconnect_uri() builder, by design — a wc: URI is minted by the WalletConnect SDK during the handshake (it carries a session-specific symmetric key), not something an application composes. cuere's job is simply to render the URI your client hands you as a scannable QR:

from cuere import scheme_case, show

# Straight from your WalletConnect client (topic and symKey shortened here):
pairing = "wc:c9e6f7a1@2?relay-protocol=irn&symKey=0123abcd"

show(pairing)             # render it exactly as issued
scheme_case(pairing)      # SchemeCase.SIGNIFICANT
    █▀▀▀▀▀█ ▄█ ▄▄█ ███▀▀▀ █▀▀▀▀▀█    
    █ ███ █ ▄█▀█▄▄█▀▀▄ ▀▄ █ ███ █    
    █ ▀▀▀ █ ▄ █ █ ▄▄█  ▀▀ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀ ▀ █ █▄▀ █ ▀ ▀▀▀▀▀▀▀    
    █▀▀▀█▄▀▀██▀ ███ ██▄▄█▀▄▀▄▀ █▄    
    ▀▄▄   ▀▄██▄ ▀ ▄▀▀▀  ██▄ █▀▀▄▄    
    ▄██ █▀▀██▀▄▄▀█▄▀█▀▄█ ▀▄▄█▀█▀█    
    ▄▀▄▀▀▀▀▀▄█▄▄▄ █▀▄▀▀ ▀ █▀██ ▀▄    
    ▄▄  ▀ ▀▀  ▀▄▀▀▄▀ █▄██ ▄▀▄▄▀█▄    
    █ ▀ █ ▀ ▀█▄▄█  ▀▄▀  ▀█▄▀█▀  ▄    
    ▀    ▀▀ ▄ ▀▄▀▀▄ ▄█▄ █▀▀▀██▀ ▄    
    █▀▀▀▀▀█ ▀▀  ▄ ▄▀  ▄██ ▀ █▄▀▄     
    █ ███ █ █▄▄▄▀▀▄▀██▀ ▀▀▀▀▀▄▀▀▄    
    █ ▀▀▀ █ █▀█▄▄▀ █▀  ▀█▀▀█▀ ▀█     
    ▀▀▀▀▀▀▀ ▀▀▀  ▀   ▀ ▀   ▀▀▀▀      

The symKey is case-sensitive and the pairing is one-shot, so the URI must be encoded byte-for-byte as issued. That is exactly why scheme_case classifies wc: as SchemeCase.SIGNIFICANT and optimize_uri leaves it untouched (see below) — case-folding it would break the connection. Real wc: topics and keys are 32-byte (64 hex-character) values from the SDK; they are shortened above only to keep the illustration compact.

Shrinking codes: optimize_uri & scheme_case

optimize_uri makes a code smaller when — and only when — that is safe. QR codes have a compact alphanumeric mode that covers digits, uppercase A–Z, and a few symbols. A bech32 address (bitcoin: / lightning:) is case-insensitive, so uppercasing it loses nothing yet lets the whole payload encode in alphanumeric mode, shaving modules off the result:

from cuere import bitcoin_uri, optimize_uri, show

# A 62-character bech32 (taproot) address — long enough that switching to
# alphanumeric mode drops a whole QR version.
addr = "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
show(optimize_uri(bitcoin_uri(addr)))
# -> BITCOIN:BC1P...  (a smaller code than the lowercase original)
    █▀▀▀▀▀█ █  ▄▄▄█▄ ▀  ▀ █▀▀▀▀▀█    
    █ ███ █ ██▄ ▀█ ▀▄  ▄▀ █ ███ █    
    █ ▀▀▀ █ ▄██ ██▄ ▀▀ ▀▄ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀ █▄▀▄▀▄▀ ▀ ▀ ▀▀▀▀▀▀▀    
    ▀█▄ ██▀▄▄ ▀ ▀▄█▄  █▀▄▄ ▀ ███▀    
    ▀▄▀█▄ ▀ ▀█▀▄█▀▄▄  █▄ ▀ █▀ █▀▀    
    ▄ █▄ ▀▀ █▄▄▀█ ▄  ▄█ ▀▀▀▄▀█▄ ▀    
    ▄▄█ █▄▀█▀▄▄▄ █ ▄ ██▀█▄▀ ▄▀▄▄▀    
    █▀█▄ ▄▀██▀█▀▀▄▀█▄ █▀▄ █ █▄ ▄▀    
      █▄▄▀▀ █▄█▄▄██▀▀█ ██ █ ▀█▄██    
    ▀▀  ▀▀▀▀█▀█▄█▀▄█▀ ▀██▀▀▀█▄  ▄    
    █▀▀▀▀▀█ ▄███▀▄▄▄██▀ █ ▀ █▀▀▀▀    
    █ ███ █ ▀▀██▄▀█▀▄ ▀ █▀▀█▀█▀ ▄    
    █ ▀▀▀ █ ▄▄▄ ▀█ ▀▄▀▄█▄▀ ▀█▄▄█▀    
    ▀▀▀▀▀▀▀ ▀▀     ▀▀▀▀  ▀  ▀▀▀      

In byte mode that lowercase address needs QR version 4 (a 41×21 half-block code); uppercased into alphanumeric mode it drops to version 3 — the 37×19 code above, the same payload in fewer modules. Once a URI carries a query string (an amount, label, or message) it is no longer alphanumeric and optimize_uri returns it unchanged, so passing any URI through it is always safe.

optimize_uri only ever uppercases a URI whose scheme is case-insensitive. scheme_case exposes that decision as a typed SchemeCase, so you can tell why a URI will or won't be optimized:

from cuere import SchemeCase, scheme_case

scheme_case("bitcoin:bc1q...")     # SchemeCase.INSENSITIVE  -> optimize_uri may uppercase
scheme_case("lightning:lnbc1...")  # SchemeCase.INSENSITIVE
scheme_case("ethereum:0xAbC...")   # SchemeCase.SIGNIFICANT  -> EIP-55 case matters; left as-is
scheme_case("wc:topic@2?...")      # SchemeCase.SIGNIFICANT  -> WalletConnect key; left as-is
scheme_case("mailto:hi")           # SchemeCase.UNKNOWN      -> not recognized; left as-is

Only SchemeCase.INSENSITIVE URIs are candidates, and even those are returned unchanged unless they are already lowercase with no query string. ethereum: and wc: are recognized explicitly as case-significant rather than incidentally passed through. See lightning-uri.md for the full scheme model.