How it works
A transfer out of a smart wallet is a contract call, and the network rejects any memo on a contract call. Exchanges also don't credit contract transfers. So Portaj makes the last leg a classic payment from an account that is yours, created for you with sponsored reserves.
One flow
1. You paste the exchange address + memo and sign in with a passkey 2. WebAuthn PRF ──► 32 bytes ──► ed25519 key ──► your own G address 3. tx1 Sponsor: BeginSponsoring ─► CreateAccount(G, 0 XLM) ─► ChangeTrust(USDC) ─► EndSponsoring 4. tx2 Smart wallet (C) ── SEP-41 transfer(C ─► G, amount) ──► USDC lands in G 5. tx3 G ── classic Payment(USDC) + memo ──► exchange; the sponsor fee-bumps it 6. Receipt: three explorer links; G holds 0 XLM; its reserves show the sponsor
First carry: three transactions. After that: two. USDC sits in your transit account for about one ledger. Portaj never holds your keys or your money; the only server key is the sponsor's.
Transactions
- tx1 · setup (first carry only)Source: Sponsor
- beginSponsoringFutureReserves(G) · createAccount(G, 0) · changeTrust(USDC) from G · endSponsoringFutureReserves from G. Signed by the sponsor and by G.
- tx2 · transfer inSource: Sponsor (relay)
- One invokeHostFunction: USDC SAC transfer(C, G, amount). Your passkey signs the wallet's auth entry. No memo: the network forbids it here.
- tx3 · payment outSource: G, fee-bumped by the sponsor
- One payment(USDC, amount) to the exchange, memo attached (ID or text). G signs the inner transaction; the sponsor's fee bump pays the fee.
- tx4 · return (recovery)Source: G, fee-bumped by the sponsor
- SAC transfer(G, C, balance) back to the smart wallet, or a plain USDC payment to a G wallet.
The sponsor service signs only these shapes. It relays a transfer only when it is a USDC transfer from a C-address into an account it sponsors, refuses any source-account authorization, and fee-bumps only single-operation transactions whose source is an account it sponsors.
Your transit account's key
salt = SHA-256("portaj:stellar:g-account:v1") // WebAuthn prf.eval.first
seed = HKDF-SHA256(ikm = prf.results.first,
salt = "portaj",
info = "stellar-ed25519-seed:" + hex(SHA-256(network passphrase)),
len = 32)
G = Keypair.fromRawEd25519Seed(seed)Same passkey, same G, every time. Testnet and mainnet give different accounts. The seed is computed in your browser and never stored or sent. Every passkey prompt on Portaj asks for the PRF value, so the prompt that signs the transfer in also unlocks the key that signs the payment out: one prompt per carry once your account exists.
When something fails
- PRF unsupported
- A one-time key held in the tab. You save a recovery file before any money moves, and keep the page open.
- Sponsor low on funds or rate-limited
- A plain message. Nothing moved.
- tx1 or tx2 fails
- Nothing moved. Try again.
- tx2 lands, tx3 fails
- USDC is in your transit account. Resume the carry, or return it to your wallet.
- Exchange has no USDC trustline
- Blocked before you sign.
- Memo required but missing (SEP-29)
- Blocked before you sign.
- Look-alike USDC
- Only Circle's issuer is accepted.
- Different device, different PRF value
- The derived account won't exist. Use the device you started on; the funds stay in the original account.
Testnet addresses
- Exchange simulator deposit
- GCMGLKBKAKL7G6XTSWPQN56MMR4KCSTMNBSSUK3IRAKSJWUA6JV4AYE4
- USDC issuer (Circle, testnet)
- GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
- USDC contract (SAC)
- CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
- Smart wallet WASM (passkey-kit)
- 97ce047884106b1c6c3bb40b8973cc48db1c4dad95c9e20462bf2c701daa764e
Verify it yourself
- On Carry, “Try the normal way” returns the network's own rejection. It matches stellar-core's validateSorobanMemo rule.
- On stellar.expert, your transit account shows 0 XLM, with the account and trustline reserves sponsored by the address above.
- The payment out carries the memo, goes to the deposit address, and its fee is paid by a fee bump.
- The exchange simulator credits it under your memo. A memo-less or contract transfer shows up as not credited.
Limits
- Testnet only. No real exchange runs on testnet, so the simulator stands in for one. Real exchanges behave this way per SDF documentation.
- PRF works with iCloud Keychain (Safari 18 / iOS 18+, macOS 15+), Google Password Manager (Chrome 132+), Windows Hello on Windows 11 25H2+, 1Password, YubiKey 5 and Proton Pass. It doesn't with a Chrome local profile, Bitwarden or Dashlane.
- Safari's cross-device (QR) flow can return a different PRF value than on-device. Use the device you started on.
- A PRF-derived key is a signing key for a transit account that holds funds for one ledger, not an encryption key for data. The usual warning is about the second.
- Carries from your own smart wallet on another site (Another wallet) need you to send the USDC to your transit account yourself.
Sources
- SDF smart wallet docs · “transfers from contracts are not supported by exchanges today”
- stellar-protocol #1950 · muxed C-addresses, a proposal
- stellar-protocol #1956 · the trustline and XLM wall
- SEP-29: fixing memo-less payments
- passkey-kit