You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A payer who already holds USDC on Stellar cannot pay through Useroutr. The checkout only offers EVM chains, and the API rejects anything else outright:
// payments.service.ts — selectCryptoif(source.kind!=='evm'){thrownewBadRequestException(`Only EVM source chains are supported in v1; got ${source.kind}`,);}
So the one payer already standing on our settlement chain has to bridge off Stellar and back through CCTP to pay a Stellar merchant. That is the worst possible route for the cheapest possible payment.
stellar is present and enabled: true in domains.ts, but only as a CCTP destination (domain 27). Nothing consumes it as a source.
Why this is worth doing before more CCTP work
1. It is the best UX in the product, by a distance. No bridge, no burn, no attestation wait, no PROCESSING limbo. A Stellar payment settles in one ledger close — around 5 seconds — against the 30s–2min of a CCTP round trip. It is also the cheapest: no gas on a source chain, no bridge fee.
2. It is the only flow where the escrow contract's guarantee is real. This is the part that matters for #180. A Stellar-native payer has an account that can authorize lock, and an address the contract can refund to on a dispute. A CCTP payer has neither, which is why #183 had to stop calling their hold "escrow".
And it composes better than a bolt-on: for a Stellar-native payer, the escrow lock is the payment. The payer signs one transaction that moves their funds into the contract with themselves as payer, the merchant as merchant, and the relay as arbiter. No holding account, no extra hop, no custody question — and a dispute genuinely refunds the person who paid.
3. It unblocks the contract we already deployed.CDKAIND4CJUC4SNVLSXS5CH5GOMNQPBU4F6I2DY4ZFNO7LKP4HM3YAIK is live, tested (51 unit tests, plus an on-chain lock → dispute → resolve smoke run) and reachable by nothing. #180's escrow-backed branch is written and unreachable for exactly this reason.
What already exists
Worth knowing before scoping — this is less work than it looks:
Quotes handle it. QuotesService already short-circuits same-chain/same-asset to a 1:1 rate and otherwise finds a Stellar path payment.
The missing pieces are payer-side: wallet connection, a non-CCTP branch in selectCrypto, and detecting the incoming payment.
Scope
Checkout — offer Stellar as a source chain and connect a wallet (Freighter at minimum).
API — branch selectCrypto on source.kind === 'stellar' instead of rejecting it. Return what the payer needs to sign rather than CCTP approve/burn calldata.
Two payment shapes, depending on the merchant's settlementHoldEnabled:
hold off → payer sends USDC straight to the merchant's settlement address
hold on → payer signs escrow.lock(payer=their address, merchant=settlement address, arbiter=relay, …), and the funds are genuinely escrowed
Detection — confirm the payment landed and drive the status transitions. Options below.
Fee — route through fee_collector as the CCTP path does.
Status transitions — a Stellar-native payment has no bridge leg, so SOURCE_LOCKED/PROCESSING may not map. Probably PENDING → QUOTE_LOCKED → COMPLETED, but that is a decision.
How do we detect the payment? Client submits the tx hash (simple, trusts the client to report), we watch Horizon for the destination account (robust, needs a watcher), or the payer calls a contract we can observe (cleanest for the escrow case, since lock emits an event). These have different failure modes when the payer closes the tab mid-flow.
XLM as well as USDC? The quote engine can path-pay, but it adds slippage the CCTP path does not have.
Does the escrow-on shape change the quote? Funds reach the merchant a window later, which the checkout should probably say out loud.
Acceptance
A payer with a Freighter wallet can pay a Stellar merchant end to end on testnet
The payment reaches COMPLETED and the merchant's balance moves by the expected amount net of fee
With the merchant's settlement hold on, the funds are locked in the escrow contract with the payer's own address as payer, and Payment.escrowId is populated
A dispute on such a payment refunds the payer's actual wallet
Problem
A payer who already holds USDC on Stellar cannot pay through Useroutr. The checkout only offers EVM chains, and the API rejects anything else outright:
So the one payer already standing on our settlement chain has to bridge off Stellar and back through CCTP to pay a Stellar merchant. That is the worst possible route for the cheapest possible payment.
stellaris present andenabled: trueindomains.ts, but only as a CCTP destination (domain 27). Nothing consumes it as a source.Why this is worth doing before more CCTP work
1. It is the best UX in the product, by a distance. No bridge, no burn, no attestation wait, no
PROCESSINGlimbo. A Stellar payment settles in one ledger close — around 5 seconds — against the 30s–2min of a CCTP round trip. It is also the cheapest: no gas on a source chain, no bridge fee.2. It is the only flow where the escrow contract's guarantee is real. This is the part that matters for #180. A Stellar-native payer has an account that can authorize
lock, and an address the contract can refund to on a dispute. A CCTP payer has neither, which is why #183 had to stop calling their hold "escrow".And it composes better than a bolt-on: for a Stellar-native payer, the escrow lock is the payment. The payer signs one transaction that moves their funds into the contract with themselves as
payer, the merchant asmerchant, and the relay asarbiter. No holding account, no extra hop, no custody question — and a dispute genuinely refunds the person who paid.3. It unblocks the contract we already deployed.
CDKAIND4CJUC4SNVLSXS5CH5GOMNQPBU4F6I2DY4ZFNO7LKP4HM3YAIKis live, tested (51 unit tests, plus an on-chain lock → dispute → resolve smoke run) and reachable by nothing. #180's escrow-backed branch is written and unreachable for exactly this reason.What already exists
Worth knowing before scoping — this is less work than it looks:
QuotesServicealready short-circuits same-chain/same-asset to a 1:1 rate and otherwise finds a Stellar path payment.StellarService.deductFeecalls the deployedfee_collector.EscrowServicein feat(api): settlement hold — merchant opt-in, ledger hold, escrow client (#180) #183 wraps lock/release/auto_release/resolve/get_escrow.The missing pieces are payer-side: wallet connection, a non-CCTP branch in
selectCrypto, and detecting the incoming payment.Scope
selectCryptoonsource.kind === 'stellar'instead of rejecting it. Return what the payer needs to sign rather than CCTP approve/burn calldata.settlementHoldEnabled:escrow.lock(payer=their address, merchant=settlement address, arbiter=relay, …), and the funds are genuinely escrowedfee_collectoras the CCTP path does.SOURCE_LOCKED/PROCESSINGmay not map. ProbablyPENDING → QUOTE_LOCKED → COMPLETED, but that is a decision.Questions to settle first
lockemits an event). These have different failure modes when the payer closes the tab mid-flow.Acceptance
COMPLETEDand the merchant's balance moves by the expected amount net of feepayer, andPayment.escrowIdis populatedRelated