A Fastify service that sits between the SwiftRamp frontend and the swiftramp-swap Soroban contract on Stellar. It does two jobs:
- API layer — read-only quotes, relaying client-signed swap transactions, checking swap status, and recent swap history for a wallet.
- Rate oracle — polls a real FX rate source on a schedule and pushes
set_rateupdates on-chain, so the contract's conversion math stays in sync with real-world rates without manual CLI calls.
This service never holds a user's private key. Swaps are built and signed client-side (e.g. via Freighter in the frontend) and only the signed transaction is sent here for submission. The one private key this service does hold is the oracle key — the contract admin's secret key, used only to sign set_rate calls on a timer.
┌─────────────┐ quote / submit signed tx ┌──────────────────┐ Soroban RPC ┌────────────────────┐
│ Frontend │ ─────────────────────────────────▶ │ swiftramp-backend│ ─────────────────────▶ │ swiftramp-swap │
│ (Next.js) │ ◀───────────────────────────────── │ (this service) │ ◀───────────────────── │ contract (Soroban) │
└─────────────┘ status / history JSON └──────────────────┘ quote/set_rate └────────────────────┘
│
│ scheduled poll (node-cron)
▼
┌──────────────────┐
│ FX rate source │
│ (open.er-api.com) │
└──────────────────┘
swiftramp-backend/
├── .env.example # Template for required environment variables
├── .env # Your actual secrets/config — never commit this
├── tsconfig.json
├── package.json
└── src/
├── server.ts # Entry point: wires up Fastify, CORS, routes, and the oracle scheduler
├── config.ts # Central env-driven config, validates required vars on boot
├── lib/
│ └── stellar.ts # All Soroban RPC logic: quote, set_rate, swap submission/status, event history
├── routes/
│ ├── quote.ts # GET /quote
│ ├── swap.ts # POST /swap/submit, GET /swap/:hash/status
│ └── history.ts # GET /history/:address
└── oracle/
└── rateOracle.ts # Scheduled job: fetches FX rates, pushes set_rate on-chain
- Node.js 18+ (for native
fetchsupport used by the rate oracle) - A deployed
swiftramp-swapSoroban contract on testnet (or mainnet), alreadyinitialized - Token contracts registered on that contract for each currency you plan to support (via
set_currency_token) - The secret key of the contract's admin account
1. Install dependencies
npm install2. Configure environment
cp .env.example .envThen fill in .env:
| Variable | Description |
|---|---|
PORT |
Port the API listens on (default 4000) |
SOROBAN_RPC_URL |
Soroban RPC endpoint (default: public testnet RPC) |
NETWORK_PASSPHRASE |
Must match the network your contract is deployed to |
SWAP_CONTRACT_ID |
Your deployed contract's C... address |
ORACLE_SECRET_KEY |
Secret key (S...) of the contract's admin account — get it with stellar keys show admin |
CURRENCY_TOKENS_JSON |
JSON map of currency code → token contract address, e.g. {"USD":"C...","NGN":"C..."} |
ORACLE_INTERVAL_MS |
How often the rate oracle runs, in milliseconds (default 5 minutes) |
3. Run in development
npm run devThis starts the server with tsx watch, runs the rate oracle once immediately, and re-runs it on the configured interval.
4. Build and run in production
npm run build
npm startLiveness check.
{ "ok": true }Read-only conversion preview, computed by simulating the contract's own quote function — so the number returned is guaranteed to match what an actual swap would produce, not a client-side copy of the rate table.
Response
{ "from": "USD", "to": "NGN", "sendAmount": "100", "receiveAmount": "158000.0000000" }Relays an already-signed transaction to the network and waits for confirmation. The frontend builds and signs this transaction itself (e.g. via Freighter) — this endpoint never sees a private key.
Body
{ "signedTxXdr": "<base64 signed transaction envelope>" }Response
{ "txHash": "a3f...", "receivedAmount": "1580000000000" }Polls the ledger for a transaction's current status.
Response
{ "status": "SUCCESS", "receivedAmount": "1580000000000" }status is one of SUCCESS, FAILED, NOT_FOUND.
Recent swap contract events involving the given address, either as sender or recipient.
Response
{
"address": "GDNSOJUOGMIOOBZVSCE2XB7F7WGBHVC3ELL3N47ANO3QFOKN4UMHCIQJ",
"swaps": [
{
"ledger": 123456,
"txHash": "a3f...",
"sender": "G...",
"recipient": "G...",
"receivedAmount": "1580000000000"
}
]
}Note: this reads live from the RPC provider's event stream, which typically only retains recent history (days, not months). For a permanent activity log, add a small database that persists events as they're observed rather than re-querying the ledger on every request.
On startup, and then every ORACLE_INTERVAL_MS, the service:
- Fetches USD-based FX rates from a free public source (
open.er-api.com, no API key required) - For each currency registered in
CURRENCY_TOKENS_JSON, converts that rate to the contract's scaled integer format - Signs and submits a
set_ratecall using the oracle key, confirming each one lands before moving to the next currency
If the FX source is unreachable, or a specific currency isn't in the response, that run logs an error and skips — it does not crash the server or stop future scheduled runs.
Swapping the FX source: the free API used here is a reasonable default but not guaranteed uptime/SLA. To switch providers, only fetchUsdRates() in src/oracle/rateOracle.ts needs to change — everything downstream (scaling, signing, submission) stays the same.
ORACLE_SECRET_KEYcan move real funds viaset_rateauthorization and should be treated like any production private key — use a secrets manager in deployment, not a plaintext.envfile on a shared server.- CORS is currently wide open (
origin: true) for development convenience. Restrict this to your actual frontend domain before deploying publicly. - This service does not rate-limit or authenticate incoming requests. Add rate limiting (e.g.
@fastify/rate-limit) before exposing it beyond local development.
- Contract:
swiftramp-smartcontract— the Soroban contract this service calls (swap,quote,set_rate, etc.) - Frontend: the Next.js app — calls
/quotefor live pricing, and will call/swap/submitonce wired up to route signed transactions through this backend instead of hitting Soroban RPC directly.