Monad Pay is a consumer payments app built on Monad testnet. It combines a self-custodial wallet flow, a virtual card onboarding flow, token approvals for settlement, collaborative vaults, and an on-chain points system.
The main idea is simple:
- users connect a wallet
- approve spending of supported tokens, with USDC as the settlement asset
- receive a virtual card
- spend through the card while the app settles the equivalent value on Monad
- earn on-chain points and use vaults for savings goals
Monad Pay has three core product flows.
Users connect a Monad wallet, complete signup details, and approve USDC for settlement. The backend then creates a virtual card record through the configured card-issuing provider and stores the provider card identifiers in Postgres.
When a card transaction is reported by the card provider, Monad Pay records the payment and then collects the equivalent amount of USDC from the user on Monad using the approval granted during signup. The collected funds are sent to the treasury wallet.
Users can create vaults backed by smart contracts and share them with other contributors. Separate contracts track Monad Pay points and a leaderboard, allowing rewards for onboarding and card activity.
- Self-custodial wallet onboarding on Monad testnet
- Virtual card signup flow
- USDC approval and settlement flow
- Portfolio view for Monad balances and supported ERC-20s
- Transaction history
- Collaborative vault creation and contribution
- On-chain points and leaderboard contracts
- PWA-style mobile-friendly UI
- Next.js App Router
- TypeScript
- Tailwind CSS
- wagmi + viem for chain interactions
- thirdweb wallet UX integration
- Next.js route handlers for signup, funding, payments, webhooks, and card retrieval
- Drizzle ORM for database access
- Postgres for persistence
- Monad testnet
- Vault factory and vault contracts
- Points token
- Admin leaderboard/minter contract
- ERC-2771 forwarder
- Stripe Issuing for cardholder and virtual card flows
- 0x for swap quoting
- Hypersync / Envio endpoint for transaction-style data queries
The codebase is currently wired for Stripe Issuing. That means local and deployed card creation requires:
- a valid
STRIPE_SECRET_KEY - a valid
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY - Stripe Issuing enabled on the Stripe account
- a configured
STRIPE_WEBHOOK_SECRETfor transaction webhooks
If Stripe Issuing is not enabled on the account, cardholder creation will fail even if the API keys are valid.
bun installcp .env.example .env.localRequired values:
DATABASE_URLNEXT_PUBLIC_THIRDWEB_CLIENT_IDTHIRDWEB_SECRET_KEYNEXT_PUBLIC_MONAD_RPC_URLMONAD_RPC_URLEXECUTOR_PRIVATE_KEYTREASURY_ADDRESSUSDC_ADDRESSNEXT_PUBLIC_USDC_ADDRESSNEXT_PUBLIC_VAULT_FACTORY_ADDRESSNEXT_PUBLIC_POINTS_TOKEN_ADDRESSNEXT_PUBLIC_AML_ADDRESSNEXT_PUBLIC_SERVER_WALLET_ADDRESSNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYSTRIPE_SECRET_KEY
Recommended values:
STRIPE_WEBHOOK_SECRETSTRIPE_ISSUING_CURRENCY=gbpENVIO_URL=https://monad-testnet.hypersync.xyz/queryZEROX_API_KEY
bun run drizzle:pushIf you want Postgres in Docker first:
bun run docker:dbbun run devExpose your app with a tunnel:
ngrok http 3000Then configure the provider webhook URL to point at:
https://your-public-url/api/webhook-stripe
/signup: wallet onboarding and token approval flow/: home dashboard with card, balance, and portfolio/transactions: activity and settlement history/vaults: vault list and vault creation flow
Key backend routes:
/api/create-user/api/get-user/api/get-user-card/api/get-card/api/fund-mon/api/execute-payment/api/webhook-stripe
This project includes contracts for:
- vault deployment and contribution
- points minting
- leaderboard tracking
- forwarder support for gasless-style flows
Contract addresses are expected through environment variables for frontend and backend use.
If local dev shows a mismatch between next and @next/swc, your dependency install is stale. Reinstall dependencies cleanly so the compiled binary version matches the app version.
If Drizzle or the app cannot connect to Postgres, verify DATABASE_URL and confirm the database is running.
If Stripe returns a message saying the account is not set up for Issuing, the API keys are valid but the Stripe account does not have Issuing enabled yet.
Make sure the configured STRIPE_WEBHOOK_SECRET matches the endpoint secret for the exact webhook destination you are testing.
- Next.js
- TypeScript
- Tailwind CSS
- wagmi
- viem
- thirdweb
- Drizzle ORM
- Postgres
- Solidity
- Foundry
- Stripe Issuing