Settle is a Next.js app that bridges fiat and Stellar USDC in both directions:
- Offramp — Stellar USDC → Base (via Allbridge) → fiat payout (via Paycrest)
- Onramp — fiat (via Paycrest) → custodial Base hot wallet → Stellar USDC (via Allbridge), delivered to the user's Stellar address
Server-side state (order status, bridge progress, platform stats) is backed by Upstash Redis; the browser only keeps a local transaction-history cache.
- Next.js 15 (App Router)
- React 19 + TypeScript
- Tailwind CSS v4
- Stellar SDK + Stellar Wallets Kit (wallet discovery, signing, WalletConnect on mobile)
- Allbridge Bridge Core SDK (Base ⇄ Stellar bridging)
- viem (Base chain transfers, custodial hot wallet)
- Upstash Redis (order/status stores)
- Telegram Bot API (operational alerts + manual retry commands)
- Connect Stellar wallet via Stellar Wallets Kit (Freighter, LOBSTR, xBull, Albedo, Hana, Ledger on desktop; Freighter/LOBSTR mobile over WalletConnect)
- Get a USDC → fiat quote, verify recipient bank account
- Build and sign the Stellar → Base bridge transaction (XDR)
- Submit to Stellar Horizon, poll bridge status
- Execute payout server-side (Base USDC transfer + Paycrest order)
- Poll payout status via SSE stream
- User pays fiat into a Paycrest-issued virtual account
- Paycrest settles USDC into the platform's custodial Base hot wallet (Paycrest has no native Stellar support)
- Server bridges Base → Stellar to the user's address once settled
- Destination defaults to the connected wallet, but the user can opt into a different Stellar address via a checkbox in the form
- Hold-and-alert on failure: if the bridge can't complete (e.g. hot wallet out of Base ETH for gas), funds stay put, the order is marked
bridge_failed, and a Telegram alert asks for manual resolution — nothing is ever auto-refunded or silently retried - A backstop cron (
/api/cron/finalize-onramp) and the open SSE session both watch in-flight bridges and flip orders todeliveredonce Allbridge confirms on Stellar
- Rich per-transaction alerts for every offramp/onramp status change (bank details, amount, rate, payout, Stellar address)
- Critical alerts for anything requiring manual action (e.g.
bridge_failed) - A stuck onramp bridge can be retried directly from Telegram — no admin app needed — by sending
/retry <orderId> [amount]to the configured chat (see Manual Bridge Retry below)
npm installcp .env.example .env.localSet values in .env.local:
Paycrest
PAYCREST_API_KEYPAYCREST_WEBHOOK_SECRET
Offramp bridge (Stellar → Base)
BASE_PRIVATE_KEYBASE_RETURN_ADDRESS/NEXT_PUBLIC_BASE_RETURN_ADDRESSBASE_RPC_URL(optional, defaults tohttps://mainnet.base.org)STELLAR_SOROBAN_RPC_URL/NEXT_PUBLIC_STELLAR_SOROBAN_RPC_URL(optional)STELLAR_HORIZON_URL(optional)
Onramp bridge (Base → Stellar, custodial)
ONRAMP_HOT_WALLET_ADDRESS/ONRAMP_HOT_WALLET_PRIVATE_KEY— platform-operated Base wallet; Paycrest delivers onramp USDC here, so it must be funded with a little Base ETH for gasONRAMP_MIN_GAS_ETH(optional, default0.0005) — refuses to bridge below this ETH balance rather than risk a stranded tx
Storage / stats
UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKENSTATS_SEED_USERS/STATS_SEED_VOLUME(seed values shown before real activity accumulates)
Telegram alerts + admin
TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID— leave blank to disable alerts entirelyTELEGRAM_WEBHOOK_SECRET— verifies inbound Telegram commands (see below)ADMIN_API_SECRET— protects the HTTP bridge-retry endpoint
Cron
CRON_SECRET— Vercel Cron sends this as a Bearer token to/api/cron/finalize-onramp
Wallet connect
-
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID— from dashboard.walletconnect.com (now also branded Reown). Required for mobile: phones have no browser extensions, so mobile wallets are only reachable over WalletConnect. Desktop extensions still work without it.Add every origin the app is served from to that project's allowed domains — including tunnels used for mobile testing. An origin that isn't listed fails at connect time with WebSocket close code
3000 — origin not allowed, which surfaces in the UI as a domain-allowlist message.
npm run devOpen http://localhost:3000.
If an onramp bridge is stuck — most commonly because the hot wallet ran out of Base ETH for gas — fund the hot wallet, then retry the bridge for that order id. Both paths share the same lock-guarded, idempotent logic (src/lib/onramp/retry-bridge.ts), so retrying a bridge already in flight is a safe no-op.
Via Telegram (register once per deployment):
curl -G "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
--data-urlencode "url=https://<your-deployment>/api/webhooks/telegram" \
--data-urlencode "secret_token=$TELEGRAM_WEBHOOK_SECRET"Then, in the configured chat:
/retry <orderId> [amount]
amount is only needed if the order never made it to settled in the store (so no USDC amount was persisted) — pass the settled USDC amount from the Paycrest webhook/dashboard in that case.
Via HTTP:
curl -X POST https://<your-deployment>/api/admin/onramp/retry-bridge \
-H "Authorization: Bearer $ADMIN_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"orderId":"<orderId>","amount":"<optional override>"}'POST /api/offramp/quoteGET /api/offramp/currenciesGET /api/offramp/institutions/[currency]POST /api/offramp/verify-accountPOST /api/offramp/paycrest/order— create the Paycrest payout orderGET /api/offramp/paycrest/order/[orderId]GET /api/offramp/status/[orderId]GET /api/offramp/stream/[orderId]— SSE status stream
POST /api/offramp/bridge/build-txGET /api/offramp/bridge/gas-fee-optionsPOST /api/offramp/bridge/submit-sorobanGET /api/offramp/bridge/status/[txHash]GET /api/offramp/bridge/tx-status/[hash]
POST /api/onramp/order— create the Paycrest onramp order; persists the user's destination Stellar addressGET /api/onramp/order/[orderId]GET /api/onramp/stream/[orderId]— SSE status stream, also drives fast-path bridge finalizationGET /api/onramp/bridge/status/[txHash]
POST /api/webhooks/paycrest— routes onramp vs. offramp by looking up which store the order lives in (Paycrest's payload has no reliable direction field)POST /api/webhooks/telegram— inbound/retrycommand handling
GET /api/cron/finalize-onramp— backstop sweep of in-flight onramp bridges
POST /api/admin/onramp/retry-bridge— manually re-trigger a stuck onramp bridge
GET/POST /api/stats— platform stats (seeded + accumulated)
- Server (Upstash Redis) is the source of truth for anything that survives a page close or webhook retry:
- Onramp order state + Base→Stellar bridge progress (
onramp:order:*,onramp:pending-bridges) - Offramp payout status (
payout:*) and order metadata for alert enrichment (paycrest:order-meta:*) - Platform stats (seeded values + real accumulated activity)
- Onramp order state + Base→Stellar bridge progress (
- Browser
localStorage(stellaramp_transactions, max 50 records, scoped by connected wallet) is a client-side history cache only — not authoritative.
npm run dev
npm run build
npm run start
npm run lint- Onramp is custodial by design between fiat settlement and Stellar delivery: Paycrest has no native Stellar support, so USDC always transits the platform's Base hot wallet before being bridged onward. The hold-and-alert failure policy exists specifically to keep that window safe — funds are never auto-refunded or blindly retried.
- Keep
.env.local,.next, andnode_modulesout of version control (already covered by.gitignore).