Accept USDC on Solana and USDT on Polygon, settling directly into the merchant wallet.
0.5% per transaction (waived during beta) · Zero custody · On-chain verified.
Onramp lets merchants generate checkout links, share them with customers, and receive stablecoin payments that settle directly into their own wallets. The system never holds funds — every payment is an on-chain transfer from payer to merchant, verified server-side before anything is marked "paid".
The platform is composed of three layers:
- Browser SPA (Vite + React 19) — marketing site, merchant dashboard, customer checkout.
- Supabase backend — Postgres with a state-machine schema, 16 migrations, Row-Level-Security lockdown, eight Edge Functions (Deno), and
pg_cronfor scheduled jobs. - Chain layer — Solana (SPL transfer + Memo-program binding) and Polygon (ERC-20 transfer with a sub-cent reference suffix).
Nothing the browser says about price, destination, amount, or status is trusted. Every transition is either produced by the server or verified by the server.
- Signup + login with auto-created profile & rotated API/webhook secrets (server-side trigger).
- Product management — create, list, delete, and copy shareable
/checkout/:productIdlinks. - Dashboard — revenue, transaction count, product count, active in-flight checkout sessions.
- Transactions ledger — full filterable history, explorer links (Solscan / Polygonscan), confirmed/failed state badges.
- Webhook configuration — set destination URL (HTTPS-only; loopback / RFC-1918 / metadata IPs blocked server-side), reveal API secret on demand, rotate webhook secret with one click, live delivery history.
- Settings — business name, Solana wallet (base58 validated), Polygon wallet (EIP-55 checksum validated).
- Phantom-based Solana USDC flow with Memo-program binding (
onramp:<reference>), destination-ATA auto-creation when the merchant hasn't received USDC before. - MetaMask-based Polygon USDT flow with chain-id check (rejects non-137) and sub-cent reference suffix so parallel sessions for the same product can never collide.
- Polls the server verifier every 3 s; the browser never invents a "confirmed" state.
- Eight Edge Functions (
create-checkout-session,verify-payment,webhook-dispatcher,update-merchant-config,get-merchant-secrets,rotate-webhook-secret,solana-rpc,v1REST API). - Public REST API v1 — API-key-authenticated endpoints for checkout sessions and transactions (
sk_live_…/sk_test_…), with idempotency keys and per-merchant rate limits. - Test mode —
sk_test_keys drive Solana devnet / Polygon Amoy verification so integrations can be exercised without real funds. - Email receipts — optional customer email on checkout; queued, retried, idempotent delivery after confirmation.
- State-machine DB —
checkout_sessions.status ∈ {awaiting_payment, confirming, confirmed, failed, expired}with a trigger that enforces legal transitions. pg_cronfires the webhook dispatcher every 10 s for delivery + catch-up verification of stuck sessions.- Token-bucket rate limiting (atomic
rl_consumeRPC) applied per-IP, per-product, and per-session on customer endpoints. - HMAC-SHA256 webhook signatures (
Onramp-Signature: t=<unix>,v1=<hex>) with replay-window enforcement on the receiver side. - Row-Level Security —
anonandauthenticatedhave zero INSERT/UPDATE/DELETE on payment tables; SELECT scoped tomerchant_id = auth.uid()where appropriate.
┌──────────────────────────────────────────────────────────────────────────┐
│ Browser (SPA) │
│ Landing · Login · Register · Dashboard · Products · Transactions │
│ · Webhooks · Settings · Checkout(/:id) · Success(?session=) │
└─────────────┬────────────────────────────────────────────────────────────┘
│ fetch(Edge Function) ▲
│ │ merchant webhook
┌─────────────▼─────────────────────────────────────────────────┐ HMAC │
│ Supabase Edge Functions (Deno) │ │
│ │ │
│ Public / anon-callable: │ │
│ • create-checkout-session (server-frozen amount, token, │ │
│ destination, reference) │ │
│ • verify-payment (drives the state machine; │ │
│ idempotent ledger insert) │ │
│ │ │
│ Dashboard / user-JWT + CORS allowlist: │ │
│ • update-merchant-config (EIP-55, base58, URL validation) │ │
│ • get-merchant-secrets (reveal sk_live_… / whsec_… ) │ │
│ • rotate-webhook-secret │ │
│ │ │
│ Scheduler-invoked (service role): │ │
│ • webhook-dispatcher ──── HMAC-SHA256 signed POST ─────────┘ │
│ exponential backoff + jitter │
└─────────────┬──────────────────────────┬─────────────────────────────────┘
│ service role │ pg_cron every 10s
▼ │
┌──────────────────────────────────────────────────────────────────────────┐
│ Postgres (Supabase) │
│ │
│ checkout_sessions → state machine + trigger-enforced transitions │
│ transactions → append-only ledger (UNIQUE network, tx_hash) │
│ webhook_deliveries → retry queue with backoff state │
│ merchant_secrets → sk_live_ / whsec_ (RLS-locked, never reaches browser)│
│ profiles, products → merchant data │
│ rate_limit_buckets → token-bucket state for rl_consume │
│ │
│ Extensions: pgcrypto · pg_cron · pg_net │
└─────────────┬────────────────────────────────────────────────────────────┘
│ fetch() from verify-payment
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Chain verifiers │
│ Solana RPC (Memo binding check) · Polygon RPC (exact amount check) │
└──────────────────────────────────────────────────────────────────────────┘
| Property | How it's enforced |
|---|---|
| Browser cannot choose what it pays | create-checkout-session loads price from products.price_minor, destination from profiles.*_wallet, freezes them into checkout_sessions. The browser receives the frozen row; anything it tampers with is compared server-side on verify. |
| On-chain binding of the session to the tx | Solana: the transaction must include a Memo-program instruction containing onramp:<reference>. Polygon: the transferred amount_minor must exactly match the server-allocated value including a unique sub-cent suffix. A random replay of an unrelated transfer fails both checks. |
| Idempotent ledger | transactions has a UNIQUE (network, tx_hash) constraint. verify-payment upserts; duplicate confirmations are no-ops. |
| State-machine enforced by trigger | checkout_sessions_enforce_transition rejects any status update that isn't a legal transition of awaiting_payment → confirming → confirmed|failed|expired. |
| Merchant secrets never in the browser | merchant_secrets.secret_key and webhook_secret have REVOKE SELECT from anon + authenticated; the dashboard reveals them via the auth-gated get-merchant-secrets Edge Function only. |
| Webhook payload authenticity | HMAC-SHA256 over <ts>.<raw_body> with the merchant's whsec_…. Signature header is Onramp-Signature: t=<unix>,v1=<hex>. Receiver example (Node + WebCrypto) ships in the Webhooks page. |
| Webhook URL validation | Server-side: HTTPS only; blocks loopback (127.0.0.0/8, ::1), RFC-1918 ranges, link-local, and cloud-metadata IPs. |
| CORS allowlist on dashboard endpoints | update-merchant-config, get-merchant-secrets, rotate-webhook-secret reject any Origin not in DASHBOARD_ORIGINS. Customer endpoints stay * for embeddability. |
| Rate limits | Per-IP (30/min), per-product (60/min) on session creation; per-IP (120/min) and per-session (12/min) on verification. Atomic token bucket via rl_consume RPC. |
| Signup trigger | SECURITY DEFINER with locked search_path = public, pg_temp; generates pk_live_…, sk_live_…, whsec_… with extensions.gen_random_bytes. Browser only supplies business_name via signup metadata. |
| Password policy | password_min_length = 10, must include upper + lower + digit, enforced by Supabase Auth server-side and mirrored client-side. |
Full Supabase security-advisor output is reviewed in supabase/README.md.
| Layer | Technology | Notes |
|---|---|---|
| Frontend | React 19, TypeScript 6, Vite 8 | tsc -b --noEmit clean |
| Styling | Tailwind CSS 3, Inter + Instrument Serif | OKX-inspired dark design system; .glow-card, .okx-*, .liquid-glass utilities |
| Icons | Lucide React | |
| Routing | React Router 7 | Nested routes under DashboardLayout |
| Auth & DB | Supabase (Postgres 17) | 16 SQL migrations tracked in supabase/migrations/ |
| Edge Functions | Deno, hosted on Supabase | 8 functions in supabase/functions/ |
| Cron | pg_cron + pg_net |
10-second schedule for webhook dispatcher |
| Crypto | pgcrypto |
All secrets generated server-side via gen_random_bytes |
| Solana | @solana/web3.js 1.98 + @solana/spl-token 0.4 |
SPL transfer + Memo-program binding |
| Polygon | ethers v6 |
ERC-20 transfer through BrowserProvider (MetaMask RPC) |
| Hosting | Vercel (frontend), Supabase (backend) | Frontend auto-redeploys on push to main |
onramp/
├── public/ # Static assets
├── src/
│ ├── assets/ # Images
│ ├── hooks/
│ │ └── useMobile.ts # Breakpoint hook
│ ├── layouts/
│ │ └── DashboardLayout.tsx # Sidebar + outlet shell
│ ├── lib/
│ │ ├── supabase.ts # Supabase client (fail-loud on missing env)
│ │ ├── auth.tsx # AuthProvider + useAuth
│ │ └── api.ts # Typed client for Edge Functions
│ ├── pages/
│ │ ├── Landing.tsx # Marketing homepage
│ │ ├── Login.tsx · Register.tsx # Merchant auth
│ │ ├── Dashboard.tsx # Stats overview
│ │ ├── Products.tsx # Product CRUD + copy link
│ │ ├── Transactions.tsx # Ledger viewer
│ │ ├── Webhooks.tsx # URL config, secret rotate, delivery history
│ │ ├── Settings.tsx # Profile + wallet addresses
│ │ ├── Checkout.tsx # Customer payment flow
│ │ └── Success.tsx # Post-payment confirmation
│ ├── App.tsx · main.tsx · index.css
│ └── …
├── supabase/
│ ├── README.md # Backend deploy + apply-order
│ ├── functions/
│ │ ├── _shared/ # auth, cors, db, hmac, validators, ratelimit,
│ │ │ # verify-solana, verify-polygon, reference
│ │ ├── create-checkout-session/
│ │ ├── verify-payment/
│ │ ├── webhook-dispatcher/
│ │ ├── update-merchant-config/
│ │ ├── get-merchant-secrets/
│ │ ├── rotate-webhook-secret/
│ │ └── solana-rpc/ # method-whitelisted RPC proxy for checkout
│ └── migrations/ # 16 migrations, 0002 → 0017
│ ├── 0002_checkout_sessions.sql # state machine + RLS lockdown
│ ├── … # webhooks, cron, rate limits, signup
│ │ # trigger, security hardening, capability
│ │ # reads, idempotency keys, test mode
│ ├── 0016_email_receipts.sql
│ └── 0017_backfill_orphan_merchants.sql
├── .env.example
├── tailwind.config.js
├── vite.config.ts
├── tsconfig*.json
└── package.json
Prereqs: Node ≥ 18, npm ≥ 9. You can run the SPA locally pointed at the live Supabase project without deploying anything.
git clone https://github.com/zanni098/onramp.git
cd onramp
npm installCreate .env:
# Required
VITE_SUPABASE_URL=https://hkheayotxkyfgxjaoizj.supabase.co
VITE_SUPABASE_ANON_KEY=<anon jwt from Supabase project>
# Optional — client-side Solana RPC override. When unset, checkout routes
# through the `solana-rpc` Edge Function proxy (server-side Helius key).
VITE_HELIUS_API_KEY=npm run dev # http://localhost:5173
npm run build # type-check + production bundle
npm run preview # serve the production build
npm run lintIf you're forking this and want your own Supabase project, see supabase/README.md for:
- Migration apply order (0002 → 0017).
- Edge Function secrets:
HELIUS_API_KEY,ALCHEMY_API_KEY,DASHBOARD_ORIGINS. - The auth-config settings that are NOT captured in migrations:
mailer_autoconfirm = truefor dev (flip tofalse+ configure a real SMTP before taking real customers),password_min_length = 10,password_required_characters = lower+upper+digit.
Frontend deploys via Vercel's GitHub integration — every push to main builds and promotes. The Supabase anon key is injected at build time via Vercel project env vars.
| Path | Component | Access | Description |
|---|---|---|---|
/ |
Landing |
Public | Marketing homepage |
/login |
Login |
Public | Merchant sign-in |
/register |
Register |
Public | Merchant registration |
/checkout/:productId |
Checkout |
Public (capability URL) | Customer payment page |
/success |
Success |
Public | Re-reads session status from DB; cannot be faked |
/dashboard |
Dashboard |
Auth | Revenue, txn count, product count, active checkouts |
/products |
Products |
Auth | Create / delete products, copy checkout link |
/transactions |
Transactions |
Auth | Filterable ledger (confirmed / failed) |
/webhooks |
Webhooks |
Auth | URL config, secret rotate, delivery history, verification examples |
/settings |
Settings |
Auth | Business name + wallet addresses (server-validated) |
Genuinely open items (the misleading "TODO" roadmap of the previous README covered things that are already shipped — those have been removed):
- Custom SMTP provider (Resend / Postmark / SendGrid) — currently
mailer_autoconfirm = trueso the free Supabase shared sender's 2/hr cap doesn't block signups. - Sentry (or equivalent) error + cron monitoring — right now a stopped cron goes undetected until webhook delivery visibly backs up.
- Hosted iframe / embed script so merchants can drop a checkout widget into their site without the
/checkout/:idredirect. - Mobile wallet deep-links (Phantom / MetaMask app) on the checkout page.
- Scrypt/argon2 hashing of
merchant_secrets.secret_key— currently plaintext-equivalent and only ever read through the auth-gated Edge Function, but should move to hashed compare once a public merchant API exists. - HIBP leaked-password protection — requires Supabase Pro plan.
- More stablecoins (USDT on Solana, USDC on Polygon, DAI).
- Platform fee collection — pricing is announced at 0.5% per transaction, but collection is not yet enforced on-chain (a fee-split requires an extra transfer instruction on Solana and a splitter contract on Polygon). Until that ships, the fee is explicitly waived: merchants keep 100%, and every pricing surface says so.
-
Analytics charts— 30-day revenue chart shipped on the Overview page.
Fork, branch, open a PR. Conventional Commits preferred for messages so changelogs are generable.
git checkout -b feat/your-feature
git commit -m "feat: ..."
git push origin feat/your-featureMIT — see LICENSE.
Built with ⚡ by zanni098 · deployed at onramp-delta.vercel.app