A non-custodial USDC payment gateway built natively on Base.
Accept stablecoin payments in your storefront β no intermediaries, no custody, no friction.
Traditional payment processors charge 2.9% + $0.30 per transaction, enforce 3-7 day settlement windows, and require merchants to pass KYB checks that take weeks. For small and emerging brands, these costs and delays are prohibitive.
BasePaymentGateway eliminates all of that. It routes USDC payments directly from a customer's wallet to the merchant's wallet in a single atomic transaction β no middlemen, no custody, instant settlement. It's the payment infrastructure that Base was built for.
This project is a working proof of concept: a premium e-commerce storefront (ELARA) that accepts USDC payments on Base through a smart contract, using Coinbase OnchainKit for wallet connectivity, Circle's USDC as the settlement currency, and a Node.js + Supabase worker to track and index orders automatically.
π Live App: https://elarapay.xyz Network: Base Sepolia Testnet
Contract:0x43EE62E72CDf8CD941AD8e7c20e8B384f6b3D684
USDC (Sepolia):0x036CbD53842c5426634e7929541eC2318f3dCF7e
| Feature | Details |
|---|---|
| Escrow Model | Funds are held in the contract until the admin marks the order as shipped, then released to the merchant. |
| USDC Native | Built for Circle's USDC on Base. Stable, predictable, and denominated in dollars. |
| Backend-Created Orders | Orders are created on-chain by the backend (admin) with a fixed price β the frontend never sets the price. |
| Replay Protection | Each orderId can only be paid once. Prevents double-charge attacks at the contract level. |
| ReentrancyGuard + CEI | Uses OpenZeppelin's ReentrancyGuard and follows the Checks-Effects-Interactions pattern. |
| Order Limits (M-02) | Configurable minOrder / maxOrder bounds enforced on-chain. |
| EIP-2612 Permit | Supports gasless approvals via payWithPermit for a better UX. |
| Rate Limiting | IP and wallet-based rate limiting via Upstash Redis to prevent abuse. |
| Distributed Locking (M-01) | Redis-based distributed lock prevents nonce racing on concurrent order creation. |
| Gas Optimized | Custom errors instead of require strings. Immutable state variables. Minimal storage footprint. |
| OnchainKit Integration | Coinbase Smart Wallet support via @coinbase/onchainkit β one-click wallet connection. |
| Event-Driven Fulfillment | Emits PaymentReceived events that a dedicated Node.js backend listens to and indexes into Supabase. |
| Admin Controls | Mark orders as shipped (releases funds), process refunds, rescue stuck tokens, pause/unpause. |
| ETH Rejection (M-05) | Contract rejects unexpected ETH transfers to prevent accidental loss. |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ELARA Storefront β
β (Next.js + OnchainKit + wagmi) β
β β
β ββββββββββββ ββββββββββββββββ βββββββββββββββββ β
β β Connect β β Product Card β β Transaction β β
β β Wallet β β + Size/Color β β Status β β
β β (Smart) β β Selection β β (On-chain) β β
β ββββββ¬ββββββ ββββββββ¬ββββββββ βββββββββββββββββ β
β β β β
βββββββββΌββββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β β
β βββββββββββββΌββββββββββββ
β β Two Transactions β
β β (Batched by OCK) β
β β β
β β 1. USDC.approve() β
β β 2. gateway.payFor() β
β βββββββββββββ¬ββββββββββββ
β β
βββββββββΌββββββββββββββββββΌββββββββββββ Base Network ββββββ
β β β (Emits PaymentReceived)
β βββββββββββββΌββββββββββββ β
β β BasePaymentGateway βββββββ€
β β β β
β β β’ Validates order β βΌ
β β β’ Marks fulfilled β βββββββββββββββββββββββββββ
β β β’ Emits event β β Node.js Worker β
β β β’ transferFrom β β β (ethers.js + Supabase) β
β β merchant wallet β β β
β βββββββββββββββββββββββββ β β’ Listens via WebSocketsβ
β β β’ Decodes event data β
βΌ β β’ Saves order to DB β
βββββββββββββ ββββββββββββ βββββββββββββββββββββββββββ
β Coinbase β β USDC β
β Smart β β (Circle) β
β Wallet β β ERC-20 β
βββββββββββββ ββββββββββββ
- Customer connects their Coinbase Smart Wallet via OnchainKit
- Customer selects a product, size, and color
- Frontend calls the backend API (
/api/create-order) which creates the order on-chain with a fixed price (backend is source of truth) - OnchainKit batches two calls into one user approval:
USDC.approve(gateway, amount)β authorize the gateway to spendgateway.payForOrder(amount, orderId)β execute the payment
- The contract validates the order, marks it as Paid, emits
PaymentReceived, and holds USDC in escrow - Admin calls
markShipped()to release funds to the merchant, orrefund()to return USDC to the buyer - A Node.js backend worker listens for the event via Alchemy WebSockets and indexes into Supabase
| Technology | Purpose |
|---|---|
| Solidity ^0.8.20 | Contract language |
| Foundry (Forge) | Testing, compilation, and deployment |
| USDC (Circle) | Payment settlement currency |
| Technology | Purpose |
|---|---|
| Next.js 14 | React framework with App Router |
OnchainKit @coinbase/onchainkit |
Wallet connection, identity, and transaction components |
| wagmi v2 | React hooks for Ethereum |
| viem | TypeScript-first EVM interactions |
| TailwindCSS | Utility-first styling |
| Technology | Purpose |
|---|---|
| Next.js API Routes | Server-side order creation and management |
| Upstash Redis | Rate limiting and distributed locking (nonce racing prevention) |
| nanoid | Cryptographically secure order ID generation |
| Supabase | PostgreSQL database for order persistence and idempotency |
| Technology | Purpose |
|---|---|
| Node.js | Runtime environment |
| ethers.js v6 | WebSocket provider and contract interactions |
| Supabase | PostgreSQL database for storing order and payment records |
Base/
βββ contracts/ # Foundry project
β βββ src/
β β βββ BasePaymentGateway.sol # Core payment gateway contract (escrow model)
β βββ test/
β β βββ Gateway.t.sol # Comprehensive Forge test suite
β βββ script/
β β βββ DeployGateway.s.sol # Deployment script
β βββ foundry.toml # Forge config (Etherscan V2 verification)
β βββ .env.example # Environment variable template
β
βββ web/ # Next.js storefront
β βββ src/
β β βββ app/
β β β βββ page.tsx # Main storefront (ELARA)
β β β βββ layout.tsx # Root layout with Providers
β β β βββ globals.css # Design system + animations
β β β βββ api/
β β β βββ create-order/
β β β βββ route.ts # Backend order creation API
β β βββ components/
β β β βββ Providers.tsx # wagmi + OnchainKit + React Query
β β βββ lib/
β β βββ contracts.ts # ABI + contract addresses
β β βββ products.ts # Product catalog (source of truth)
β β βββ redis.ts # Upstash Redis client
β βββ package.json
β
βββ worker/ # Node.js Event Indexer
β βββ index.js # Listens to on-chain events and pushes to Supabase
β βββ package.json
β
βββ .gitignore
βββ README.md
A single, focused contract that manages the full order lifecycle: Create β Pay β Ship/Refund, with USDC held in escrow.
Core Functions:
| Function | Access | Description |
|---|---|---|
createOrder() |
Owner | Creates an order with fixed price, bound to a specific buyer |
payForOrder() |
Buyer | Pays for an existing order (requires prior USDC approval) |
payWithPermit() |
Buyer | Pays using EIP-2612 permit (gasless approval) |
markShipped() |
Owner | Releases escrowed USDC to the merchant |
refund() |
Owner | Returns escrowed USDC to the buyer |
setLimits() |
Owner | Sets min/max order amount bounds |
rescueERC20() |
Owner | Rescues stuck tokens from the contract |
pause() / unpause() |
Owner | Emergency pause mechanism |
getOrder() |
Public | View helper to query order details |
Security Audit Fixes Applied:
| ID | Severity | Fix |
|---|---|---|
| M-01 | Medium | Distributed lock (Redis) prevents nonce racing in backend |
| M-02 | Medium | On-chain minOrder / maxOrder bounds |
| M-03 | Medium | rescueERC20() to recover stuck tokens |
| M-04 | Medium | Explicit allowance check after permit try/catch |
| M-05 | Medium | receive() reverts to reject unexpected ETH |
| B-01 | Backend | Validate order amount against contract bounds before createOrder |
| B-02 | Backend | Per-wallet rate limiting via Upstash Redis |
| B-03 | Backend | Cryptographically secure order IDs via nanoid |
| I-01 | Info | getOrder() view helper for easier integrations |
| I-03 | Info | Rescued event emitted on token recovery |
Additional Security:
- OpenZeppelin
Ownable2Stepβ Two-step ownership transfer prevents accidental loss - OpenZeppelin
ReentrancyGuardβ Protects all state-changing external functions - OpenZeppelin
Pausableβ Emergency circuit breaker - CEI Pattern β State changes before external calls
- Custom errors β Gas-efficient error handling (no string storage)
- Immutable USDC β
usdcaddress set once at deploy time
The contract is tested with 10 test cases covering:
| Category | Tests |
|---|---|
| Positive flows | Successful payment, multiple orders, allowance checks |
| Input validation | Zero amount, empty order ID |
| Allowance checks | Insufficient approval, insufficient balance |
| Replay protection | Same order ID from same buyer, same order ID from different buyers |
| State verification | Balance assertions, event emission, fulfillment mapping |
cd contracts
forge test -vvv- Node.js v18+
- Foundry (for smart contracts)
- A Coinbase Developer Platform API key
git clone https://github.com/iarturo/ElaraPay.git
cd ElaraPaycd contracts
cp .env.example .env
# Edit .env with your deployer key and USDC addressCompile:
forge buildTest:
forge test -vvvDeploy to Base Sepolia (with verification):
source .env && forge script script/DeployGateway.s.sol:DeployGateway \
--rpc-url $BASE_SEPOLIA_RPC_URL \
--broadcast \
--verify \
--verifier etherscan \
--verifier-url "https://api.etherscan.io/v2/api?chainid=84532" \
--etherscan-api-key $BASESCAN_API_KEYcd web
npm installCreate a .env.local file:
NEXT_PUBLIC_ONCHAINKIT_API_KEY=your_coinbase_api_key
NEXT_PUBLIC_GATEWAY_ADDRESS=0x43EE62E72CDf8CD941AD8e7c20e8B384f6b3D684
NEXT_PUBLIC_USDC_ADDRESS=0x036CbD53842c5426634e7929541eC2318f3dCF7e
NEXT_PUBLIC_CHAIN=sepolia
NEXT_PUBLIC_ALCHEMY_ID=your_alchemy_api_keyCreate a .env.server.local file (server-side secrets):
ADMIN_PRIVATE_KEY=0xYourAdminPrivateKey
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your_supabase_service_key
UPSTASH_REDIS_REST_URL=your_upstash_redis_url
UPSTASH_REDIS_REST_TOKEN=your_upstash_redis_tokenRun the development server:
npm run devOpen http://localhost:3000 to see the storefront.
The worker listens for on-chain events and logs payments to Supabase.
cd worker
npm installCreate a .env file in the worker directory:
ALCHEMY_WSS=wss://base-sepolia.g.alchemy.com/v2/your_alchemy_api_key
GATEWAY_ADDRESS=0x43EE62E72CDf8CD941AD8e7c20e8B384f6b3D684
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_SERVICE_KEY=your_supabase_service_role_key
MERCHANT_WEBHOOK_URL=https://merchant.example/webhooks/elarapayRun the worker:
node index.jsWhen MERCHANT_WEBHOOK_URL is set, each indexed payment sends:
{
"orderId": "ord_...",
"amount": 42,
"buyer": "0x...",
"txHash": "0x...",
"timestamp": "2026-05-30T00:00:00.000Z"
}Failed webhook deliveries retry 3 times with exponential backoff.
| Network | Contract | USDC Address | Chain ID |
|---|---|---|---|
| Base Sepolia | 0x43EE62E...D684 |
0x036CbD53842c5426634e7929541eC2318f3dCF7e |
84532 |
| Base Mainnet | Not yet deployed | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
8453 |
| Platform | URL |
|---|---|
| Frontend | https://elarapay.xyz |
| Contract (BaseScan) | View on BaseScan |
To switch to mainnet, update the USDC address in your .env and change the chain configuration in Providers.tsx from baseSepolia to base.
- Refund mechanism β Admin-initiated refunds with on-chain audit trail
- Webhook notifications / indexing β Node.js worker listens to events and pushes to Supabase
- Multi-token support β Accept ETH and other ERC-20s alongside USDC
- Merchant dashboard β Real-time order tracking via event indexing
- Multi-merchant support β Route payments to different merchants per product
- Mainnet deployment β Production launch on Base Mainnet
This project is licensed under the MIT License. 2026
Built with π on Base β bringing the world onchain, one payment at a time.