Skip to content

Stellar-native payments: let a payer who already holds USDC on Stellar pay without bridging #184

Description

@0xdevcollins

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:

// payments.service.ts — selectCrypto
if (source.kind !== 'evm') {
  throw new BadRequestException(
    `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:

The missing pieces are payer-side: wallet connection, a non-CCTP branch in selectCrypto, and detecting the incoming payment.

Scope

  1. Checkout — offer Stellar as a source chain and connect a wallet (Freighter at minimum).
  2. API — branch selectCrypto on source.kind === 'stellar' instead of rejecting it. Return what the payer needs to sign rather than CCTP approve/burn calldata.
  3. 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
  4. Detection — confirm the payment landed and drive the status transitions. Options below.
  5. Fee — route through fee_collector as the CCTP path does.
  6. 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.

Questions to settle first

  • Which wallets? Freighter is the obvious first. Worth checking whether the passkey work in PR 7.9c — Passkey-derived self-custody settlement wallet (Approach B) #150 should land first so this is built against smart wallets rather than retrofitted.
  • 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
  • The flow is covered by an E2E test

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend API workblockchainSmart contracts and chain integrationscritical-pathBlocks other work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions