SwiftRamp is a non-custodial currency conversion app built on the Stellar network. It lets someone send value in one currency and have it arrive in another, with every transfer wrapped in a zero-knowledge proof so the amount and resulting balances are never exposed on-chain.
This README documents how the app is structured today, how the privacy story is implemented in the current codebase, and what each page does.
Two things are true about every transfer in SwiftRamp:
- Non-custodial. SwiftRamp never holds funds or private keys. The user connects their own Stellar wallet (Freighter) and signs every transaction themselves.
- Shielded. Before a transfer settles, the app walks through a proof-generation sequence — committing the amount, generating a zk-SNARK, and verifying it — so that only validity is checked on-chain, not the amount itself.
Point 1 is real: wallet connection goes through the actual @stellar/freighter-api package. Point 2 is currently simulated in the UI — see Section 3 for exactly what's real versus presentational.
| Layer | Choice |
|---|---|
| Framework | Next.js (App Router), TypeScript, React |
| Styling | Plain CSS-in-JS via <style dangerouslySetInnerHTML> per page — no CSS framework |
| Fonts | Space Grotesk (display), IBM Plex Sans (body), IBM Plex Mono (data/labels), loaded via Google Fonts @import |
| Wallet | @stellar/freighter-api — browser-extension wallet for Stellar |
| Network (intended) | Stellar |
| Client state | useState/useEffect, no global store; wallet address persisted to localStorage |
Install the one non-default dependency before running the wallet flow:
npm install @stellar/freighter-apiThis is the part worth reading closely before presenting the app as "already private."
- Wallet connection (
/get-started) calls the real Freighter API:isConnected(),isAllowed(),setAllowed(),getAddress(). If the extension isn't installed or the user declines, the UI reflects that accurately. - Non-custody: there is no backend wallet, no server-side key storage, and no code path that could move funds without the user's own Freighter signature.
The "zero-knowledge proof" shown during a transfer is a UI simulation, not a working zk-SNARK circuit:
randomHex(48)generates a random hex string and displays it as0x{proofHex}— this is cosmetic, not a cryptographic commitment or proof.- The three proof stages (
Committing amount→Generating zk-SNARK proof→Verifying on-chain) are advanced on fixedsetTimeoutdelays (480ms + i * 620ms), not by any actual circuit computation or network round-trip. - The final "Sent" screen shows a hardcoded placeholder transaction reference (
stellar.expert/tx/a3f...9bc) — no transaction is actually submitted to Stellar. - Exchange rates (
ratesobject: NGN, KES, GHS, ZAR, USD, EUR, GBP) are hardcoded constants, not fetched from a live rates feed.
To make the privacy story real, the pieces that need to be built are:
- A Pedersen commitment scheme for the transfer amount, computed client-side.
- A zk-SNARK circuit (e.g. via a library such as
snarkjs/circom, or a Stellar-compatible proving system) that proves the commitment is well-formed and the sender has sufficient balance, without revealing the amount. - An on-chain or off-chain verifier that checks the proof before/alongside the Stellar transaction, so the ledger only ever sees "proof valid," not the amount.
- A real Stellar transaction (via
@stellar/stellar-sdkor similar), signed through Freighter, replacing the simulated success screen. - A live FX rate feed replacing the hardcoded
ratesobject.
Everything described in Section 5 below is accurate to the current UI/UX; just keep the distinction above in mind when describing the product's privacy guarantees externally.
Handled entirely on /get-started:
idle → checking → (no-wallet | requesting) → (connected | error)
checking: callsisConnected(). If Freighter isn't installed or not connected, moves tono-walletwith an install link.requesting: callsisAllowed(); if not yet allowed, callssetAllowed()to trigger the Freighter approval popup.connected: callsgetAddress(), stores the address inlocalStorageunderswiftramp_stellar_address, and auto-redirects to/swapafter 1.4s.error: surfaces whatever message Freighter or the catch block returned, with a retry button.
The /swap page reads swiftramp_stellar_address from localStorage on mount to decide whether to show the connect prompt or the transfer form, and provides a Disconnect action that clears it.
Note: localStorage here only stores a public wallet address for session convenience — never a key or secret.
| Route | File (App Router) | Purpose |
|---|---|---|
/ |
src/app/page.tsx |
Marketing homepage. Hero, live-style stats, an animated globe/network graphic showing the 7 supported currencies as nodes, and an inline converter card that runs the full simulated send → proof → success flow. |
/swap |
src/app/swap/page.tsx |
The actual transfer screen. Requires a connected wallet (checks localStorage); shows a ticker tape of currency pairs, a "celestial dial" background graphic, and the same send → proof → success flow as the homepage card, gated behind wallet connection. |
/get-started |
src/app/get-started/page.tsx |
Wallet connection flow described in Section 4. Redirects to /swap on success. |
/how-it-works |
src/app/how-it-works/page.tsx |
Explains the 5-step transfer process (connect → set amount → proof generated → verified on-chain → settled) as a horizontal numbered pipeline, plus an FAQ accordion covering what the proof hides, custody, and why Stellar. |
/rates |
src/app/rates/page.tsx |
A live-style rate board (all supported currencies against USD), a 3-up fee/spread/settlement-time summary, and a small conversion previewer that links into /get-started. |
/company |
src/app/company/page.tsx |
Mission statement, a "where we operate" map (the homepage's globe graphic repurposed with named cities), four stated product principles, and a closing CTA. |
Shared component:
| Component | File | Purpose |
|---|---|---|
Navbar |
components/Navbar.tsx |
Fixed top nav used on every page except /swap (which has its own minimal top bar). Links to How it works / Rates / Company, plus a persistent "Get started" CTA. Responsive: collapses to a burger menu under 760px. |
Defined redundantly per-page today (candidate for extraction into a shared lib/ module):
const rates: Record<string, number> = {
NGN: 1580, KES: 130, GHS: 15.6, ZAR: 18.9, USD: 1, EUR: 0.93, GBP: 0.79
}
const flags: Record<string, string> = {
NGN: '🇳🇬', KES: '🇰🇪', GHS: '🇬🇭', ZAR: '🇿🇦', USD: '🇺🇸', EUR: '🇪🇺', GBP: '🇬🇧'
}All rates are USD-relative constants. Converting A → B is computed as amount * rates[B] / rates[A].
Recommended refactor: move rates, flags, and currency metadata into a single shared module (e.g. lib/currencies.ts) so /, /swap, and /rates don't drift out of sync — they currently each define their own copy.
- Colors and spacing are defined as CSS custom properties inside each page's own
<style>block rather than a global stylesheet or Tailwind config. Core tokens repeated across pages:--ink,--paper,--accent(mint green, used for the "trusted/settlement" side of the story),--privacy(violet, used specifically for anything related to the zk-proof/shielding story),--line,--muted,--fill. - The mint/violet split is intentional: mint = network/settlement/trust, violet = privacy/proof. Keep this convention if adding new UI.
- Motion (
fade-up, pulsing dots, animated proof spinner) is used to signal "live" and "in progress" states — none of it is purely decorative.
- Replace simulated proof generation with a real commitment + zk-SNARK pipeline.
- Replace the hardcoded transaction hash on the success screen with a real Stellar transaction submission and explorer link.
- Replace static
rateswith a live FX feed. - Extract
rates/flags/currency metadata into a shared module. - Add a
/public/images/logo.pngasset —Navbar.tsxreferences it vianext/imageand will 404 without it. - Wire real anchors/routes for the mobile nav panel's "Get started" button (currently just closes the panel).