DeenBridge runs on Stellar testnet by default (STELLAR_NETWORK=testnet).
This document is the complete checklist for moving the payment stack to
mainnet (the Stellar public network, STELLAR_NETWORK=public or
mainnet). Following it end-to-end means the switch requires no code
reading — only environment changes, wallet/trustline setup, and a smoke
test.
⚠️ Config is validated at boot. The backend validates the Stellar configuration at startup (seesrc/config/stellar.js). A wrong or incomplete configuration fails fast with an error naming the exact problem instead of failing later on the first Horizon call. If you see❌ Stellar configuration errorin the logs at boot, fix the named variable and restart — do not ship it.
Everything network-dependent resolves from a single source of truth
(src/config/stellar.js):
| Setting | testnet | mainnet (mainnet / public) |
|---|---|---|
| Network passphrase | Test SDF Network ; September 2015 |
Public Global Stellar Network ; September 2015 |
| Default Horizon URL | https://horizon-testnet.stellar.org |
https://horizon.stellar.org |
| USDC issuer (Circle) | GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 |
GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN |
| EURC issuer | GB3Q6QDZYTHWT7E5PVS3W7FUT5GVAFC5KSZFFLPU25GO7VTC3NM2ZTVO |
GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2 |
STELLAR_NETWORK accepts testnet, mainnet, or public (public is the
SDF name for the production network and is treated as mainnet). The USDC
issuer and default Horizon URL are derived from the network — you cannot
accidentally combine a mainnet flag with a testnet issuer or Horizon URL; the
boot-time validation rejects it.
Change these in the backend deployment (.env / Render / Vercel env):
# The one switch that matters
STELLAR_NETWORK=mainnet # or "public" — both mean mainnet
# Optional: explicit Horizon endpoints (comma-separated, for redundancy).
# Leave UNSET to use the network default (https://horizon.stellar.org).
# Never point a mainnet deployment at the testnet Horizon URL — boot will fail.
# HORIZON_URLS=https://horizon.stellar.org,https://horizon-fr.stellar.org
# Horizon client tuning (optional, same defaults as testnet)
# HORIZON_TIMEOUT_MS=10000
# HORIZON_MAX_RETRIES=3
# HORIZON_CB_THRESHOLD=5
# HORIZON_CB_COOLDOWN_MS=30000Also confirm the platform-level keys are set for mainnet (they are network-agnostic, but they move real money now):
# Platform fee wallet (receives the platform share of a fee-split purchase)
PLATFORM_WALLET_PUBLIC_KEY=G...
# Donation fund destination
DONATION_WALLET_PUBLIC_KEY=G...
# SEP-10 auth keypair public key (published in stellar.toml)
SIGNING_KEY=G...PLATFORM_FEE_PERCENT,PLATFORM_COLLECT_ENABLED— unchanged.- The secret keys of user wallets are never stored on the backend (non-custodial). Users hold their own funds.
The frontend must run on the same network or signatures will be rejected
(wrong network passphrase). In the dnb-frontend deployment set:
NEXT_PUBLIC_STELLAR_NETWORK=mainnetThis must match the backend's STELLAR_NETWORK exactly. A mismatch
(backend on mainnet, frontend on testnet) produces signatures that fail with
op_bad_auth / bad network passphrase on submit.
Creators receive USDC directly to their own wallets (direct settlement) or the platform wallet receives it (platform-collect mode). For a creator to be able to receive USDC on mainnet, their wallet must have a USDC trustline to the mainnet Circle issuer:
USDC issuer (mainnet): GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN
Notes:
- A trustline added on testnet does not carry over to mainnet — trustlines are per-account and per-network. Every creator must add the mainnet USDC trustline even if they already had one on testnet.
- Freighter / xBull / Albedo have a "manage assets" / "add asset" flow; pasting the issuer above adds the trustline. This costs a small one-time XLM reserve (the account must also hold a bit of XLM for fees and the reserve).
- A purchase to a creator without the USDC trustline fails on-chain
(
op_no_trust). The preflight check (POST /api/stellar/payment/preflight) surfaces this before the wallet is asked to sign, and the initialize endpoint can return{ fallback: "claimable_balance" }so the buyer can still complete via a claimable balance instead of dead-ending. - If the platform is in platform-collect mode, the platform wallet itself must have the mainnet USDC trustline.
# Replace G... with the creator's public key
curl "https://horizon.stellar.org/accounts/G..." | jq '.balances[] | select(.asset_code=="USDC")'An entry with asset_code: "USDC" and asset_issuer equal to the mainnet
Circle issuer confirms the trustline exists. If the array is empty of USDC,
the creator needs to add it.
Run these in order, on the mainnet deployment, after the env changes are live. Do not proceed past a failed step.
- Boot check — start the backend. Confirm it logs
✅ Environment variables validated successfullyand no❌ Stellar configuration error. A badSTELLAR_NETWORKvalue or a mainnet/testnet Horizon or issuer mismatch aborts startup with the exact variable named.curl -s http://localhost:5000/health # -> {"success":true,"message":"pong"} - Network sanity — confirm the app resolves mainnet:
and, from the code,
curl -s http://localhost:5000/.well-known/stellar.toml | grep -i networkNETWORK/networkPassphraseresolve toPublic Global Stellar Network ; September 2015. - Creator trustline — pick a test creator, confirm their mainnet USDC trustline via the Horizon query above. If missing, have them add it.
- Buyer setup — a test buyer connects a mainnet wallet with a small amount of USDC (≥ item price) and enough XLM for fees/reserve.
- Preflight — call
POST /api/stellar/payment/preflightfor a paid course. Expectsuccess: truewith nodestination_no_trustlinereason. - Initialize —
POST /api/stellar/payment/initializereturns unsigned XDR +expectedHash. Confirm the returnednetworkPassphraseis the public passphrase. - Sign & submit — the wallet signs the XDR on mainnet;
POST /api/stellar/payment/submitreturns"Payment successful!"with a mainnetstellar.expertexplorer URL. - On-chain verify — open the explorer link. Confirm a USDC payment to the
creator (or platform) and that the buyer now owns the item
(
GET /api/stellar/payment/transactionsshows itconfirmed). - Creator received USDC — confirm the creator's mainnet USDC balance increased by the expected amount (minus platform fee if enabled).
To go back to testnet, revert STELLAR_NETWORK=testnet (backend) and
NEXT_PUBLIC_STELLAR_NETWORK=testnet (frontend) and redeploy. Both sides must
change together. Testnet and mainnet data (transactions, balances) are
completely separate — records created on mainnet are not visible on testnet
and vice versa.
- Stellar docs: Networks, Claimable balances
- Circle USDC: USDC on Stellar
- Config source of truth:
src/config/stellar.js, asset registry:src/config/assets.js
Scholarship contributions use a Soroban escrow contract and USDC Stellar Asset
Contract (SAC). They are separate from Sadaqah, which remains a direct classic
Stellar payment to DONATION_WALLET_PUBLIC_KEY. Switching the payment stack to
mainnet does not deploy or initialize a scholarship escrow.
For a mainnet scholarship campaign, first deploy and initialize a new contract
on mainnet using the reviewed workflow in contracts/README.md. Derive the USDC
SAC address for the mainnet Circle issuer; never reuse a testnet SAC or escrow
contract ID. Then configure the backend deployment with:
STELLAR_NETWORK=mainnet
SOROBAN_RPC_URL=https://<trusted-mainnet-soroban-rpc>
SCHOLARSHIP_ESCROW_CONTRACT_ID=C...Configure NEXT_PUBLIC_STELLAR_NETWORK=mainnet in the frontend. The contract
must be initialized with the intended mainnet USDC SAC address, beneficiary,
arbiter, immutable milestone amounts, and expiry ledger. Confirm all public
addresses and the refund/release policy before a donor signs a transaction.
The backend prepares wallet-signed transactions; it must not hold donor or
arbiter secret keys. Keep Sadaqah destination and configuration independent of
these scholarship settings.
Do not advertise scholarship funding as live until the mainnet contract ID is configured and the dashboard can read its initialized state from the selected mainnet Soroban RPC. Verify funding, milestone release, and post-expiry refund with a deliberately small live transaction and the public explorer.
Scholarship applicant selection is a private, off-chain process documented in
scholarship-applications.md. Keep the round
closed until its eligibility rules are public. Select a beneficiary through at
least two independent admin reviews before initializing the immutable escrow;
then review award amount, milestone evidence, arbiter, expiry, and refund terms.
Never publish application statements or private evidence on Stellar.