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.L1 · Physical: the paper letter
L1 · Physical: the paper letter
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.L2 · Data link: envelope, postage, provider handoff
L2 · Data link: envelope, postage, provider handoff
The framing layer that gets a letter from AroPay onto the postal network.
POST /transfer/letter/{id}/mail hands the PDF to Lob with a
deliveryMethod of standard (USPS First Class) or tracked (First Class
plus USPS Certified for US destinations, Registered for international).
Lob prices and applies postage; there are no Forever stamps. Test-mode Lob
keys (test_…) validate everything but never print or mail, and the letter
reports mail.testMode: true.Responsibilities: provider API, idempotent submission, tracking numbers
and events, expected delivery date. Failure mode:
402 postage_unavailable (provider outage or non-address rejection),
503 mail_provider_not_configured. Recovery: the letter stays
ESCROWED with the attempt recorded; retry /mail, or print it yourself.L3 · Network: addressing and routing
L3 · Network: addressing and routing
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.L4 · Transport: escrow of encrypted value
L4 · Transport: escrow of encrypted value
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.L5 · Session: claim lifecycle
L5 · Session: claim lifecycle
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.L6 · Presentation: how the letter looks
L6 · Presentation: how the letter looks
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.L7 · Application: sender and recipient intent
L7 · Application: sender and recipient intent
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 toCLAIMING, 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
expiresAtfor 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
confidentialTransferclamps an over-spend to an encrypted zero instead of reverting, so AroPay decrypts theConfidentialTransferevent of every leg and compares it with the letter amount before moving the status. A clamped escrow becomesFAILED(400 insufficient_confidential_balance); a clamped release rolls the letter back to claimable (503 escrow_vault_underfunded). - Refunds are tracked separately from status.
refundon the letter points at theLETTER_REFUNDtransaction; 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.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, noI,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→1are corrected before the checksum is verified. Anything else is400 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’sAROPAY_PUBLIC_URL), so a reprint months later still points at the same place, and mailing requires anhttps: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
Wallet 0x8Ba1…BA72
To
Jane Doe
12 Adeola Odeku Street
Victoria Island
Near Eko Hotel
Lagos LA
Nigeria
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.
••••••• 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
status: "DRAFT" and escrow.status: "PENDING", poll GET /transfer/letter/{id};
the letter becomes ESCROWED once the receipt lands and the amount verifies.
Print and mail on the sender’s behalf
MAILED with mail.providerId, mail.status,
mail.trackingNumber, mail.expectedDeliveryDate and mail.testMode.
Preview and claim
{ 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
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
- 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. - 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"returns400 delivery_method_unavailable. - Postage is priced and applied by Lob. There are no Forever or Global
Forever stamps;
standardis USPS First Class,trackedadds Certified (US) or Registered (international). - The claim code is 28 characters, Crockford base32 with two check
symbols, printed as seven groups of four. The eight-character
6H2K-QT4Psketch is gone. - The claim URL is
https://<aropay-host>/claim#<code>on the AroPay host itself, with the code in the URL fragment. There is noclaim.aropay.aro.media/{code}subdomain and the code never travels in a path or query string. amount_visibility_invalidand theformatfield are gone. An invalidamountVisibilityis an ordinary400 validation_error; the PDF is the only format and is fetched fromGET /transfer/letter/{id}/pdf.- Screening is a country allow/block hook only. No sanctions/PEP
provider is wired in;
403 recipient_blockedis reserved for one. - 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.
- Letters are English-only. No label localisation in the MVP.
- Nothing is printed on the envelope beyond the address Lob applies;
the amount is never visible from outside, regardless of
amountVisibility. - Void is allowed after mailing, with an explicit
acknowledgeMailed: true(stop-payment semantics); the paper letter then fails with409 letter_not_claimable. - Senders cannot claim their own letter, by account or by wallet
address (
400 cannot_claim_own_letter); they void instead. - 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.Related
- Send Aro Mail: the step-by-step guide.
- Confidential transfers: how the escrow, release and refund legs stay private.
- Transactions: the
LETTER_*rows every leg writes.