A non-custodial, gasless link-drop system for sending stablecoins to anyone with a phone number.
Iris lets a sender lock stablecoins in an on-chain escrow and generate a payment link. The recipient opens the link, verifies their phone number, and claims the funds directly to their wallet — without needing to already have one set up. All cryptographic operations happen client-side; the backend never has access to the full claiming credentials.
Sending stablecoins today requires the recipient to already have a wallet address. For international remittance, this means the person you want to pay needs to have already navigated wallet setup, seed phrase backup, and chain selection before they can receive a single dollar.
There is no lightweight onboarding path. Existing solutions are either fully custodial (the platform holds the funds) or require both parties to be on-chain before the transfer can happen.
Payment links bridge this gap. The sender locks funds on-chain, and the recipient claims them when they are ready — with identity verification gating access so only the intended recipient can claim.
Four services coordinate to create and claim payment links. The green path shows the happy-path flow of funds and credentials.
The sender generates a payment link in seven steps. All cryptographic material is created client-side.
The sender connects their wallet via RainbowKit / WalletConnect. This wallet holds the stablecoins to be sent.
Claimer keypair(secp256k1) — the derived address is registered with the escrow contract as the authorized claimer.
Encryptor keypair— used to encrypt the claimer private key before sending it to the backend.
The claimer private key is encrypted using AES-256-GCM with a shared secret derived via ECDH from the encryptor keypair. The backend will store this ciphertext but can never decrypt it.
The sender approves the escrow contract to spend the specified token amount via approve(escrow, amount).
Funds are locked on-chain. The contract records the sender, claimer address, token, amount, and expiry. Returns a bytes32 escrowId.
The backend stores the encrypted claimer key, encryptor public key, and validation requirements (the recipient's phone number). Returns a UUID for the payment link.
The shareable payment link is assembled:
#). Browsers never send fragments to servers. The backend never sees this value.The recipient opens the payment link and claims the funds in nine steps. The critical security property is that the encryptor private key (from the URL fragment) never leaves the browser.
The encryptor private key is parsed from the URL fragment. This happens entirely client-side — the fragment is never sent to the server.
GET /iris/v0/payment-links/{id} returns the payment link metadata: amount, token, expiry, status. This endpoint is public and requires no authentication.
The recipient provides their phone number (must match the sender's specified recipient) and connects a wallet to receive the funds.
WIMS (POST /v0/{wallet}/identifiers) links the recipient's phone number to their wallet address, creating an on-platform identity.
Charon issues an ephemeral JWT (POST /v0/tokens) with the recipient's phone identifier. This token authorizes the claimer key request.
GET /iris/v0/payment-links/{id}/claimer-key validates the JWT's identity against the link's validation requirements (AND semantics). If the phone number matches, the encrypted claimer key is returned.
The encryptor private key from the URL fragment is combined with the encryptor public key (returned alongside the encrypted key) via ECDH to derive the shared secret. The claimer private key is decrypted using AES-256-GCM.
The client signs Claim(escrowId, recipient) using the decrypted claimer private key. This typed data signature proves authorization without the claimer key ever touching a server.
The signed claim is submitted to the escrow contract. The contract verifies the EIP-712 signature against the registered claimer address and transfers the escrowed funds to the recipient's wallet. Gas is covered by the Pact paymaster.
The system is designed so that no single party — including the Iris backend — can unilaterally claim funds.
The backend never sees the claimer private key in plaintext. It stores only the AES-256-GCM ciphertext, which is useless without the encryptor private key.
Claiming requires two pieces of information held by different parties: the encryptor private key (in the URL fragment, held by the link recipient) and the encrypted claimer key (on the Iris server). Both are needed.
The Iris backend only releases the encrypted claimer key after WIMS attestation confirms the caller's phone number matches the sender's specified recipient. Validation requirements use AND semantics.
Funds are held by the smart contract, not by any service. The sender can reclaim after expiry. No backend has withdrawal capability.
The Pact paymaster covers gas for claim transactions, so recipients do not need native tokens to receive funds. This removes the last onboarding friction for new users.
URL fragments (the part after #) are defined by RFC 3986 as client-only. Browsers, proxies, and servers never transmit the fragment in HTTP requests. The encryptor private key stays in the recipient's browser.
Two keypairs are generated per payment link using the @noble/curveslibrary. The claimer keypair is a standard secp256k1 keypair — the same curve used for Ethereum accounts. The claimer's EVM address is derived via keccak256 of the uncompressed public key and registered with the escrow contract.
The claimer private key is encrypted using AES-256-GCM with a symmetric key derived from an ECDH shared secret between the encryptor keypair. The sender's client computes sharedSecret = ECDH(encryptorPrivateKey, encryptorPublicKey), derives a 256-bit AES key via HKDF, and encrypts. At claim time, the recipient's client performs the same derivation using the encryptor private key from the URL fragment and the encryptor public key from the backend.
The claim operation requires an EIP-712 signature from the claimer private key. This is verified on-chain by the escrow contract against the registered claimer address.
Domain {
name: "PactClaimablePaymentEscrow"
version: "1"
chainId: 42220
verifyingContract: <escrow_address>
}
Type Claim {
bytes32 escrowId
address recipient
}The escrowId is a bytes32 value derived on-chain via keccak256 from the escrow parameters (sender, claimer, token, amount, nonce). This deterministic derivation means the escrow ID is known immediately after the createEscrow() transaction confirms.
Iris is part of the Pact protocol. Try the demo