Skip to main content

The idea in one paragraph

aro-mail turns a private cAROUSD transfer into a physical letter. The sender creates a letter, the value moves into escrow, and AroPay renders a one-page PDF carrying a QR code and a 28-character claim code. The sender prints it, or has AroPay print and mail it through Lob. The recipient scans the QR (or opens the link), signs in to AroPay, picks a wallet, and claims: the escrow releases cAROUSD into their wallet. From there they can hold it, transfer it privately, or redeem it for stablecoin. Unclaimed letters expire and refund the sender automatically; the sender can void an unclaimed letter at any time. AroPay delivers the digital dollars; local fiat conversion is the recipient’s responsibility.

Why it fits AroPay

  • Extends confidential transfers into an offline delivery channel: all three money legs (escrow, release, refund) are cAROUSD confidentialTransfers with FHE-encrypted amounts.
  • Reuses the sandbox’s custodial wallets, transaction ledger, receipts, and dashboard; every letter leg is a normal transaction row.
  • Reaches recipients who have no wallet or no reliable internet at send time.
  • Enterprise use cases: remittances, gifts, disaster relief, legal payments, unbanked payouts.

Scope boundary

AroPay owns

Escrow, PDF rendering, mailing through Lob, the claim page, delivery of cAROUSD to the recipient’s wallet, refunds on void or expiry, and redeem to stablecoin.

AroPay does not own

Local fiat conversion (NGN, INR, PHP, etc.), bank payouts, off-ramp partnerships, or postal transit once Lob hands the letter to the carrier. The recipient handles cash-out.

aro-mail as a 7-layer stack

The OSI model is a useful frame because aro-mail is a protocol: it takes value at one end, encodes it through several independent layers (crypto, escrow, print, postage, scan, claim), and reconstructs it at the other end. Each layer has one job and a clean handoff to the next. If a layer fails (letter lost, QR damaged, wallet not set up), only that layer needs recovery, never the whole system.
The actual sheet of paper carrying ink. AroPay owns the layout template (single US Letter page, black on cream, readable in black and white); Lob and the postal carrier own the transit.Responsibilities: print quality, QR contrast, one-time-claim warning. Failure mode: letter lost, damaged, or destroyed in transit. Recovery: the sender voids the letter (POST /transfer/letter/{id}/void) and gets the escrow back, or waits for the expiry sweep to refund it.
Global address resolution. One flexible schema for every country: ISO 3166-1 country codes, free-text or ISO 3166-2 regions, optional postal, first-class landmark (“Near …”), with stricter rules for US addresses (two-letter state, ZIP or ZIP+4, 64-character lines). Destinations are every ISO country minus the deployment’s block list (or only its allow list, when one is set).Responsibilities: address validation, country-aware rules, the recipient screening hook. Failure mode: 400 invalid_postal_address, 400 country_not_supported, 403 recipient_blocked (reserved for a future sanctions provider). Recovery: the sender fixes the address and creates a new letter; nothing was escrowed yet.
The reliable-delivery layer. POST /transfer/letter moves cAROUSD from the sender’s custodial wallet into the escrow vault: the platform operator wallet, with the ledger of who owns what kept in Postgres. Every leg is recognised only after its ConfidentialTransfer event is decrypted and the amount verified, so value cannot be lost or duplicated regardless of what happens at L1–L3.Responsibilities: debit the sender, hold the value, honour void and expiry, release to the claimant. Failure mode: 400 insufficient_confidential_balance, 502 transfer_failed, 503 escrow_vault_underfunded. Recovery: the transaction row carries the error; a failed escrow leaves the letter FAILED with nothing moved, and a failed release puts the letter back to claimable.
The one-time session that binds a physical letter to a single successful claim. A 128-bit secret is Crockford-base32 encoded with two check symbols into a 28-character code, printed as XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX and embedded in the QR as a URL fragment. Status machine below; the claim flip and every other transition is a guarded conditional update, so two scans cannot both win.Responsibilities: atomic status flips, single-use enforcement, expiry sweep, void authorisation, sender self-claim rejection. Failure mode: 409 already_claimed, 410 letter_expired, 409 letter_not_claimable, 400 cannot_claim_own_letter. Recovery: expired letters refund the sender; claimed letters are terminal.
Rendering of the escrowed transfer into something a human can read and a phone camera can decode: PDF layout, QR encoding, sealed vs. visible amount, sender and recipient blocks, expiry, and a large stamp overlay (CLAIMED, VOID, EXPIRED) once a letter is finished so a reprint can never pass as live. Letters are English-only in the MVP.Responsibilities: GET /transfer/letter/{id}/pdf, amountVisibility: sealed | visible, overlay for terminal states. Failure mode: 409 letter_not_printable (DRAFT or FAILED letters), unreadable QR. Recovery: re-download the PDF from the same letter id; the escrowed value at L4 does not change, and the printed code still works if it is typed in by hand.
What a human is actually trying to do. Sender: “pay Jane 50 privately, she has no wallet yet.” Recipient: “claim the letter I received.”Responsibilities: the Aro Mail pages in the dashboard for creating, printing, mailing, tracking and voiding letters; the public /claim page on the AroPay host for recipients; optional redeem after claim. Failure mode: the recipient scans but has no AroPay account. Recovery: accounts are provisioned by Aro Media; the code stays claimable until expiresAt, so the recipient can come back once their account exists. There is no claim reservation.

Layer boundary at claim

When the recipient scans, the layers unwind in reverse: L1 delivers paper, L6 decodes the QR, L5 validates the code and flips the letter to CLAIMING, L4 releases escrow into the recipient’s wallet and verifies the amount. L7 then decides what to do next: hold, transfer, or redeem.

End-to-end flow

Status machine

CLAIMING and FAILED are implementation states beyond the original DRAFT → ESCROWED → MAILED → CLAIMED | EXPIRED | VOIDED sketch. Claimable statuses are ESCROWED and MAILED; terminal statuses are CLAIMED, EXPIRED, VOIDED, FAILED. Rules worth knowing:
  • Every transition is guarded. Status changes are conditional updates keyed on the current status (and expiresAt for claims), so a second claimant, a concurrent void, and the expiry sweep cannot race each other into double-spending the escrow.
  • Confirmed is not paid. ERC-7984 confidentialTransfer clamps an over-spend to an encrypted zero instead of reverting, so AroPay decrypts the ConfidentialTransfer event of every leg and compares it with the letter amount before moving the status. A clamped escrow becomes FAILED (400 insufficient_confidential_balance); a clamped release rolls the letter back to claimable (503 escrow_vault_underfunded).
  • Refunds are tracked separately from status. refund on the letter points at the LETTER_REFUND transaction; a failed refund never un-expires or un-voids the letter and is retried by the sweep.
  • Senders cannot claim their own letter (400 cannot_claim_own_letter), by account or by wallet address. Recovery is /void, so the audit trail always shows a void.
  • Reads never move money. Listing letters or previewing a claim reconciles receipts and derives status, but refunds, releases and Lob calls only happen on owner detail reads, on the write endpoints, and in the sweep.

Escrow model and trust model

There is no new smart contract. The escrow vault is the platform operator wallet holding cAROUSD, and the ledger of which letter owns how much lives in Postgres. Three on-chain legs move value, each recorded as a transaction row you can list, poll and receipt:
On-chain, the amounts of all three legs are FHE-encrypted (confidentialTransfer). Off-chain, the AroPay ledger holds the cleartext letter amount, exactly as it already does for every custodial-wallet transaction. The API therefore returns amount as a plain decimal string, and observers of the chain still see only that the operator sent or received something.
The operator’s cAROUSD balance is shared between letters in flight and other operator duties. The expiry sweep reports vault solvency (vault.balance, vault.inFlight, vault.solvent) so operators can top the vault up before a release would clamp.

Claim code and claim URL

  • Secret. 16 random bytes (128 bits) encoded in Crockford base32 (0123456789ABCDEFGHJKMNPQRSTVWXYZ, no I, L, O, U) plus two check symbols derived from the secret’s SHA-256, for 28 symbols displayed in groups of four: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX.
  • Forgiving input. Codes are upper-cased, dashes and whitespace are stripped, and O→0, I→1, L→1 are corrected before the checksum is verified. Anything else is 400 invalid_claim_code.
  • Storage. Only a SHA-256 hash is used for lookup; the code itself is encrypted at rest and returned to the owner in the create and detail responses while the letter is non-terminal. It never appears in list items, in the public preview, in URL paths or query strings, or in logs.
  • Claim URL. https://<aropay-host>/claim#<code>: the code travels in the URL fragment, which browsers never send to the server. The claim page reads it, removes it from the address bar immediately, keeps it in session storage across sign-in, and never puts it in a redirect target. The QR encodes this URL. The origin is fixed when the letter is created (from the deployment’s AROPAY_PUBLIC_URL), so a reprint months later still points at the same place, and mailing requires an https: origin (400 claim_origin_not_public).
  • Manual entry. The letter also prints the code under OR ENTER CODE for recipients whose camera cannot read the QR.

What the letter looks like

Rendered mockup of a sandbox letter. Real letters use the recipient’s postal address; the QR encodes the full claim URL, with the code in the fragment.
AroPay · aro-mail
Confidential Letter of Value
LETTER ID
cklet0001abcd
From
Acme Payroll, Inc.
Wallet 0x8Ba1…BA72
To
Jane Doe
12 Adeola Odeku Street
Victoria Island
Near Eko Hotel
Lagos LA
Nigeria
Sealed amount
••••••• cAROUSD
Revealed to recipient on claim
Claim at
dev.aropay.aro.media/claim#7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y
Or enter code
7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y
Expires 2026-10-22One-time claim · Do not photograph
Scan the code or open the link, sign in to AroPay, choose a wallet, claim.
Sealed letters print ••••••• cAROUSD; visible letters print the amount and 1 cAROUSD = 1 USD. Finished letters (claimed, voided, expired) re-render with a large diagonal stamp across the page.

The API

All endpoints live under /transfer/letter and use the shared envelope. Full schemas and a playground are in the API reference; the step-by-step is in Send Aro Mail.

Create a letter

The call blocks up to ~20 s for the escrow receipt. If it returns with status: "DRAFT" and escrow.status: "PENDING", poll GET /transfer/letter/{id}; the letter becomes ESCROWED once the receipt lands and the amount verifies.
Returns the letter as MAILED with mail.providerId, mail.status, mail.trackingNumber, mail.expectedDeliveryDate and mail.testMode.

Preview and claim

Moves cAROUSD from the escrow vault into the claiming wallet and returns { letter, transaction }. Idempotent for the same claimant: calling again returns the existing claim. A sealed letter’s amount and memo are revealed here, never in the public preview.

Void

Refunds the sender while the letter is ESCROWED, or MAILED when the body carries acknowledgeMailed: true (the paper letter will then fail with 409 letter_not_claimable; AroPay also asks Lob to cancel the mailing, best effort).

Addresses, destinations and limits

Defaults

Both defaults can be overridden per letter (amountVisibility, and expiresInDays from 1 to 180).

Error codes

The full catalog, including the generic codes, is in Errors.

Decisions made for the MVP

  1. Escrow is the operator vault plus a Postgres ledger; there is no escrow contract. Every leg is a verified cAROUSD confidentialTransfer; the ledger row (SandboxLetter) is the source of truth for who owns what, and every status change is a guarded conditional update.
  2. Lob is the only fulfilment provider. PostGrid, Letter Labs and courier delivery are not offered; the provider interface exists so they can be added, and deliveryMethod: "courier" returns 400 delivery_method_unavailable.
  3. Postage is priced and applied by Lob. There are no Forever or Global Forever stamps; standard is USPS First Class, tracked adds Certified (US) or Registered (international).
  4. The claim code is 28 characters, Crockford base32 with two check symbols, printed as seven groups of four. The eight-character 6H2K-QT4P sketch is gone.
  5. The claim URL is https://<aropay-host>/claim#<code> on the AroPay host itself, with the code in the URL fragment. There is no claim.aropay.aro.media/{code} subdomain and the code never travels in a path or query string.
  6. amount_visibility_invalid and the format field are gone. An invalid amountVisibility is an ordinary 400 validation_error; the PDF is the only format and is fetched from GET /transfer/letter/{id}/pdf.
  7. Screening is a country allow/block hook only. No sanctions/PEP provider is wired in; 403 recipient_blocked is reserved for one.
  8. No claim reservation. A code stays claimable until it is claimed, voided or expired; a recipient who scans before they have an account can come back later.
  9. Letters are English-only. No label localisation in the MVP.
  10. Nothing is printed on the envelope beyond the address Lob applies; the amount is never visible from outside, regardless of amountVisibility.
  11. Void is allowed after mailing, with an explicit acknowledgeMailed: true (stop-payment semantics); the paper letter then fails with 409 letter_not_claimable.
  12. Senders cannot claim their own letter, by account or by wallet address (400 cannot_claim_own_letter); they void instead.
  13. No self-service sign-up. Recipients need an AroPay account provisioned by Aro Media; the claim page explains this and points to support@aro.media.

Availability

Not in the MVP

Lob webhooks (status is refreshed on owner reads instead), PostGrid or Letter Labs implementations, a real sanctions/PEP provider, claim reservations, self-service account sign-up, envelope printing options, per-country availability beyond allow/block lists, and localisation.