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
Approach A (auto-provisioned managed Stellar wallet at signup, PR #136) is the right onboarding model for businesses. But without a withdrawal path it's a roach motel: merchants can take payments but can't move the USDC to a wallet they control.
The "managed by default, self-custody when you want" marketing claim falls apart without this. It's the honest-custody lever that makes Approach A defensible.
This is also flagged as a known gap in `apps/api/docs/architecture/merchant-settlement-onboarding.md`.
Scope
Three deliverables — keep them in one PR if possible:
Load the managed account on Horizon → read USDC balance
If `amount === "all"`, withdraw `balance` (the merchant can't drain XLM reserves — only USDC)
Validate `amount <= balance` and `amount > 0`
Refuse if destinationAddress has no USDC trustline: pre-flight check on Horizon. Surface a clear error: "The destination address must have a USDC trustline. Help →"
Build + sign + submit a Stellar payment op (USDC, source = managed account, destination = destinationAddress)
Write a `SettlementWithdrawal` audit row: `{ merchantId, amount, destinationAddress, stellarTxHash, status: 'submitted', createdAt }` (new table — add migration)
Why
Approach A (auto-provisioned managed Stellar wallet at signup, PR #136) is the right onboarding model for businesses. But without a withdrawal path it's a roach motel: merchants can take payments but can't move the USDC to a wallet they control.
The "managed by default, self-custody when you want" marketing claim falls apart without this. It's the honest-custody lever that makes Approach A defensible.
This is also flagged as a known gap in `apps/api/docs/architecture/merchant-settlement-onboarding.md`.
Scope
Three deliverables — keep them in one PR if possible:
Backend
Endpoint
```http
POST /v1/merchants/me/settlement/withdraw
Authorization: Bearer
Content-Type: application/json
{
"destinationAddress": "G…", // valid Stellar G-strkey
"amount": "100.50", // string decimal; "all" to drain
"asset": "USDC" // only USDC in v1
}
```
Behavior
Rate limiting
3 withdrawals per merchant per hour. Use `@nestjs/throttler` with a named bucket (`withdraw`).
Tests
Frontend
Where it lives
`apps/dashboard/src/app/(dashboard)/settings/page.tsx` — extend the existing "Settlement wallet" card from PR 7.9b.
UI
In the "Settlement active" state, add below the address:
```
Balance: 124.50 USDC (refreshes every 30s)
[Withdraw to my wallet]
```
Button opens a modal:
```
Withdraw USDC
Destination Stellar address [G____________________]
↳ Must have a USDC trustline. How to add one →
Amount [____________] USDC [Max]
Available: 124.50 USDC
[Cancel] [Send 100.00 USDC]
```
Behavior
Balance polling
Add `GET /v1/merchants/me/settlement/balance` → returns `{ asset: "USDC", balance: "124.50", lastFetchedAt }` (reads Horizon, caches for 30s in Redis to avoid hammering Horizon)
Files to read first
Acceptance criteria
Out of scope (future work)
Estimated effort
~1 day (full-stack), assuming familiarity with the existing Stellar service helpers.