Cash App for USDC. Send dollars to anyone in the world by @username, settled in USDC on Stellar in seconds.
Purser is Cash App for USDC: a stablecoin wallet built for normal people, not crypto users. Users sign in with Face ID, get an @username, and send, request or split dollars with anyone in the world. No addresses, seed phrases, gas or tickers.
Underneath, every account is a Stellar smart account and every payment is USDC on Stellar. Purser is built on four open-source Soroban building blocks that any Stellar wallet can reuse:
Cash in and out runs through Stellar anchors (SEP-10, SEP-12, SEP-24).
| Purser needs | Stellar provides |
|---|---|
| Sign-in with no seed phrase | Soroban smart accounts with secp256r1 (passkey / WebAuthn) signature verification |
| Users who never hold XLM | Fee-bump transactions and sponsored reserves, paid by Purser's relayer |
| Stable, liquid dollars | Native USDC, the largest share of Stellar payment volume |
| Pay by name across wallets | SEP-2 federation |
| Local cash in and out | Anchor network via SEP-10 / SEP-12 / SEP-24 |
| Settlement in seconds for fractions of a cent | ~5 second ledger close, low fixed fees |
Stellar is not storage here: accounts, balances, payments, escrow, requests and settlement all run on-chain.
┌──────────────────────────────────────────────────────────────┐
│ PURSER APP (iOS / Android / Web) │
│ Face ID passkey · @usernames · send · request · split │
└──────────────┬───────────────────────────────┬───────────────┘
│ signed auth entries │ SEP-10/12/24
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────┐
│ PURSER BACKEND │ │ STELLAR ANCHORS │
│ · Relayer (fee-bump, pays │ │ · deposit (bank/card → │
│ all network fees) │ │ USDC) │
│ · SEP-2 federation server │ │ · withdraw (USDC → local │
│ · Indexer (contract events) │ │ currency / mobile) │
│ · Phone/email OTP service │ └─────────────┬────────────┘
└──────────────┬───────────────┘ │
│ submits Soroban txs │ USDC payments
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ STELLAR NETWORK (Soroban) │
│ │
│ [Smart Account]──owns──▶[Username Registry] │
│ passkey signer @handle → C-address │
│ │ │
│ ├──USDC──▶[Claim Escrow] phone/email claim links │
│ ├──USDC──▶[Requests/Splits] on-chain requests │
│ └──USDC──▶ any Stellar address / other Purser user │
│ │
│ USDC (Stellar Asset Contract) │
└──────────────────────────────────────────────────────────────┘
| Component | Role | Stellar tech |
|---|---|---|
| Smart account | One per user; holds USDC; authorises with the device passkey | OpenZeppelin Stellar smart account, secp256r1 verifier, __check_auth |
| Relayer | Submits user-signed operations and pays fees and reserves | Fee-bump transactions, sponsored reserves |
| Username registry | Maps @handle ↔ smart-account address | Soroban contract (Purser, open source) |
| Claim escrow | Holds USDC for recipients without accounts | Soroban contract (Purser, open source) |
| Requests & splits | Payment requests and shared bills | Soroban contract (Purser, open source) |
| Federation server | Lets other wallets pay maria*purser.app | SEP-2 |
| Anchor client | Cash in and out in local currency | SEP-10, SEP-12, SEP-24 |
| Indexer | Reads contract events for app history, monitoring and metrics | Soroban events via RPC |
All contracts are written in Rust for Soroban and released under Apache-2.0, with a TypeScript SDK.
// Username registry
fn register(env, owner: Address, handle: String) // owner.require_auth()
fn resolve(env, handle: String) -> Option<Address>
fn reverse(env, account: Address) -> Option<String>
fn transfer(env, handle: String, new_owner: Address) // owner.require_auth()
fn release(env, handle: String)
// rules: lowercase a-z0-9_, 3-20 chars, reserved-name list, one handle per account
// Claim escrow
fn create(env, sender: Address, token: Address, amount: i128,
claim_hash: BytesN<32>, expiry: u64) -> u64 // returns claim id
fn claim(env, id: u64, secret: Bytes, recipient: Address)
fn refund(env, id: u64) // after expiry, to sender
// Requests and splits
fn request(env, payee: Address, payers: Vec<Address>,
amounts: Vec<i128>, memo: String) -> u64
fn pay(env, id: u64, payer: Address) // payer.require_auth()
fn cancel(env, id: u64) // payee only
fn status(env, id: u64) -> RequestStatus
Every state change emits a contract event (registered, claimed, refunded, paid), which feeds the indexer and on-chain metrics.
@maria through the registry.create with its hash and a 14-day expiry.claim.refund returns the USDC to the sender.| Risk | Mitigation |
|---|---|
| Lost passkey device | Synced passkeys plus a second recovery signer with a time delay |
| Claim-link phishing or guessing | 256-bit secrets, only the hash on-chain, OTP to the named phone or email, expiry and refund |
| Username squatting and impersonation | Reserved names, rate limits, one handle per account, display of the full address on first payment |
| Fee-sponsorship drain by spam | Per-account and per-device quotas on the relayer, account creation gated by verification |
| Contract bugs | Unit and integration tests, independent security audit before mainnet |
A full threat model and on-chain monitoring plan follow Stellar's developer templates. Monitoring alerts on unusual contract events, relayer spend and failed claims.
Purser publishes its contract IDs, so growth is verifiable on-chain: