Receiver-controlled crypto payments via ENS — no chain coordination, no token mismatch.
Sending crypto today requires the sender to know the recipient's:
- Wallet address (or ENS name)
- Preferred chain
- Preferred token
- Whether they want privacy
If any of those don't match, the sender has to coordinate with the receiver first. That's friction that doesn't exist in traditional payments like UPI or Venmo.
ENSPay turns an ENS name into a full payment specification. Receivers publish their preferences once as ENS text records. Payers just enter a .eth name — ENSPay handles the rest automatically.
Receiver (once) Payer (every time)
────────────────────────────── ──────────────────────────────
alice.eth Enter: alice.eth + amount
enspay.token = USDC → Resolve preferences
enspay.network = base Auto-route: swap if needed
enspay.dex = uniswap Bridge if cross-chain
enspay.slippage = 0.5 Deliver to stealth address
enspay.stealth = true ↓
alice.eth receives USDC on Base
No coordination. No copy-pasting addresses. The ENS record is the payment contract.
| Feature | Description |
|---|---|
| Smart Routing | Same-chain direct transfer, same-chain token swap (Uniswap V3), or cross-chain bridge (Across Protocol v3) — picked automatically |
| Stealth Payments | Receiver enables one-time addresses; payer's identity is never on-chain linked to the recipient |
| QR Payment Terminal | UPI-style /receive page — generates a scannable QR that any payer can scan from the home screen |
| QR Scanner | Native BarcodeDetector API — no library overhead; parses URLs, bare ENS names, and EIP-681 ethereum: URIs |
| ENS Profiles Manager | View, edit, and delete all saved ENS preferences in one place, synced to MongoDB and on-chain simultaneously |
| Shareable Pay Links | /pay/alice.eth — a dedicated payment page for any ENS name, shareable as a link or QR |
| Transaction Dashboard | Volume charts, route breakdown (pay vs swap), top recipients |
┌────────────────────────────────────────────────────────────────────┐
│ Frontend — Next.js 14 │
│ │
│ / Pay page (UPI-style, QR scan, ENS input) │
│ /pay/[ens] Shareable pay-by-ENS page │
│ /receive QR payment terminal for receivers │
│ /setup Receiver preference setup │
│ /profiles View + edit all saved ENS profiles │
│ /dashboard Transaction analytics │
└────────────┬──────────────────────────────────┬───────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌───────────────────────────────┐
│ Ethereum Sepolia │ │ Base Sepolia │
│ │ │ │
│ ENS Registry │ │ ENSPayRouter.sol │
│ PublicResolver │ │ ├─ resolveAndPay() │
│ ├─ setText() │ │ │ USDC transferFrom │
│ └─ getText() │ │ └─ resolveAndSwap() │
│ │ │ Uniswap V3 → USDC │
└────────────────────┘ └───────────────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌───────────────────────────────┐
│ MongoDB Atlas │ │ Across Protocol v3 │
│ ens_profiles │ │ SpokePool.depositV3() │
│ (off-chain cache) │ │ (mainnet cross-chain bridge) │
└────────────────────┘ └───────────────────────────────┘
payer: ENS name + amount + input token
│
▼
resolve ENS text records
│
▼
same chain as receiver?
│ │
YES NO
│ │
▼ ▼
input = USDC? Across quote available?
│ │ │ │
YES NO YES NO (testnet)
│ │ │ │
▼ ▼ ▼ ▼
direct Uniswap depositV3() show error
xfer V3 swap on SpokePool
| Layer | Technology | Version |
|---|---|---|
| Smart contracts | Solidity + Hardhat | ^0.8.24 |
| Frontend | Next.js + TypeScript + Tailwind CSS | 14 / ^5.6 / ^3.4 |
| Web3 client | wagmi + viem | ^2.12 / ^2.21 |
| Wallet UI | RainbowKit | latest |
| ENS | ENS Sepolia (Registry + PublicResolver) | — |
| DEX | Uniswap V3 exactInputSingle |
0.3% fee tier |
| Cross-chain bridge | Across Protocol v3 depositV3 |
— |
| QR codes | qrcode.react QRCodeSVG |
4.2.0 |
| QR scanning | Native BarcodeDetector API |
— |
| Database | MongoDB Atlas | 7.1.0 |
enspay/
├── contracts/
│ └── ENSPayRouter.sol # USDC transfer + Uniswap V3 swap router
│
├── scripts/
│ ├── deploy.js # Hardhat deploy to Base Sepolia
│ ├── validateDeployment.js # Smoke-test deployed contract
│ └── readEns.js # Read enspay.* text records from CLI
│
├── frontend/
│ ├── pages/
│ │ ├── index.tsx # Home — QR scanner, ENS input, quick actions
│ │ ├── pay/[ens].tsx # /pay/alice.eth — shareable payment page
│ │ ├── receive.tsx # Receiver QR terminal
│ │ ├── setup.tsx # Save ENS preferences on-chain + MongoDB
│ │ ├── profiles.tsx # Manage all saved profiles
│ │ ├── dashboard.tsx # Transaction history + analytics
│ │ └── api/
│ │ ├── profile.ts # REST: CRUD for MongoDB ENS profiles
│ │ └── stealth-address.ts # REST: generate one-time stealth address
│ │
│ ├── components/
│ │ └── Layout.tsx # Global header, nav, wallet button
│ │
│ └── utils/
│ ├── ens.ts # ENS text record read/write helpers
│ ├── contracts.ts # ABIs + deployed addresses
│ ├── bridge.ts # Across Protocol v3 quote + depositV3
│ ├── mongodb.ts # MongoClient singleton + ENSProfile CRUD
│ ├── stealthStore.ts # In-memory stealth address ↔ recipient map
│ └── wagmi.ts # Wagmi + RainbowKit chain config
│
├── hardhat.config.js
├── package.json # Contracts workspace
└── frontend/package.json # Frontend workspace
ENSPay reads and writes six keys on the ENS PublicResolver (Ethereum Sepolia). All six are set atomically in a single multicall() transaction.
| Key | Example Value | Options | Description |
|---|---|---|---|
enspay.token |
USDC |
USDC, USDT, DAI, WETH |
Preferred output token |
enspay.network |
base |
base, arbitrum, ethereum |
Preferred destination chain |
enspay.dex |
uniswap |
uniswap, aerodrome, sushiswap |
Preferred DEX hint |
enspay.slippage |
0.5 |
0.1 – 50 |
Max acceptable slippage (%) |
enspay.note |
For coffee ☕ |
any string | Optional note shown to payers |
enspay.stealth |
true |
true, false |
Enable one-time stealth addresses |
ENSPayRouter.sol — view on Base Sepolia
Address: 0xD07f07f038c202F8DEbc4345626466ef4AC93b99
Network: Base Sepolia (chain ID 84532)
USDC: 0x036CbD53842c5426634e7929541eC2318f3dCF7e
SwapRouter02: 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4
/// @notice Transfer USDC directly from payer to recipient.
/// @dev Requires prior ERC-20 approval of `amount` to this contract.
function resolveAndPay(
string calldata ensName,
address recipient,
uint256 amount
) external;
/// @notice Swap `inputToken` → USDC via Uniswap V3 exactInputSingle, deliver to recipient.
/// @dev Requires prior ERC-20 approval of `amountIn` to this contract.
function resolveAndSwap(
string calldata ensName,
address recipient,
address inputToken,
uint256 amountIn,
uint256 amountOutMinimum
) external returns (uint256 amountOut);
/// @notice Apply default 0.5% slippage to a quoted output amount.
function applyDefaultSlippage(uint256 quotedOut) external pure returns (uint256);event PaymentRouted(
address indexed sender,
address indexed recipient,
string ensName,
address token,
uint256 amount
);
event SwapRouted(
address indexed sender,
address indexed recipient,
string ensName,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut
);| Constant | Value | Meaning |
|---|---|---|
DEFAULT_SLIPPAGE_BPS |
50 |
0.5% default slippage |
UNISWAP_POOL_FEE |
3000 |
0.3% Uniswap V3 fee tier |
BPS_DENOMINATOR |
10_000 |
Basis points denominator |
| Contract | Address |
|---|---|
| ENS Registry | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e |
| Public Resolver | 0x8FADE66B79cC9f707aB26799354482EB93a5B7dD |
- Node.js ≥ 18
- WalletConnect Project ID — cloud.walletconnect.com (free)
- MongoDB Atlas URI — mongodb.com/atlas (free tier works)
- An ETH Sepolia wallet with testnet funds (faucet) and Base Sepolia USDC (faucet)
git clone https://github.com/CodeswithrohStudio/enspay.git
cd enspay
# Contract workspace
npm install
# Frontend workspace
cd frontend && npm installfrontend/.env.local (frontend + API routes):
# MongoDB — off-chain profile cache
MONGODB_URI=mongodb+srv://<user>:<password>@cluster.mongodb.net/brobet
# WalletConnect
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id
# RPC endpoints
NEXT_PUBLIC_BASE_SEPOLIA_RPC=https://sepolia.base.org
NEXT_PUBLIC_ETH_SEPOLIA_RPC=https://rpc.sepolia.org
# Deployed contract addresses (already live — no deploy needed)
NEXT_PUBLIC_BASE_SEPOLIA_USDC=0x036CbD53842c5426634e7929541eC2318f3dCF7e
NEXT_PUBLIC_ENSPAY_ROUTER_ADDRESS=0xD07f07f038c202F8DEbc4345626466ef4AC93b99.env (Hardhat — only needed if redeploying contracts):
PRIVATE_KEY=0x...
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
ETH_SEPOLIA_RPC_URL=https://rpc.sepolia.orgcd frontend
npm run devOpen http://localhost:3000.
# From repo root
npm run compile
npm run deploy:base-sepoliaAfter deploy, update NEXT_PUBLIC_ENSPAY_ROUTER_ADDRESS in frontend/.env.local.
# Smoke-test the live contract
npm run validate:deployment
# Read ENS text records for any name
TEST_ENS_NAME=yourname.eth npm run read:ens- Go to
/setup - Connect wallet on Ethereum Sepolia
- Enter your ENS name (e.g.
yourname.eth) - Choose token, network, DEX, slippage, note, stealth toggle
- Click Save Preferences → single
multicall()tx on Sepolia - Preferences are also cached to MongoDB for instant reads
- Go to
/(home) - Type a
.ethname or tap Scan QR to scan the receiver's QR code - Enter amount and your input token
- ENSPay resolves preferences and shows the route (direct / swap / bridge)
- Click Pay → approve ERC-20 + confirm transaction
- Receiver gets USDC on their preferred chain
- Go to
/receive - ENSPay detects your stealth preference automatically
- Static mode — QR encodes
/?ens=yourname.eth(reusable) - Stealth mode — QR encodes a fresh EIP-681 one-time address (single use)
- Display the QR; payer scans and pays
- Go to
/profiles - See all ENS names registered from your wallet
- Click Edit on any card → change any preference inline
- Changes are saved on-chain + to MongoDB in one action
All routes are Next.js API routes under frontend/pages/api/.
| Parameter | Required | Description |
|---|---|---|
ens |
one of | Fetch a single profile by ENS name |
address |
one of | Fetch all profiles owned by a wallet address |
Response (single):
{
"ensName": "alice.eth",
"ownerAddress": "0x...",
"token": "USDC",
"network": "base",
"dex": "uniswap",
"slippage": "0.5",
"note": "For coffee",
"stealth": true,
"updatedAt": "2025-01-01T00:00:00.000Z"
}Upserts a profile. ensName and ownerAddress are required.
{
"ensName": "alice.eth",
"ownerAddress": "0x...",
"token": "USDC",
"network": "base",
"dex": "uniswap",
"slippage": "0.5",
"note": "",
"stealth": false
}| Parameter | Required | Description |
|---|---|---|
ens |
✓ | ENS name to delete |
address |
✓ | Owner address (auth check) |
Generate a one-time stealth address for the given ENS name.
Request:
{ "ensName": "alice.eth" }Response:
{
"stealthAddress": "0x...",
"eip681": "ethereum:0x...@84532/transfer?address=0x036CbD...&uint256=1000000"
}When enspay.stealth = true on a receiver's ENS:
- Payer calls
POST /api/stealth-addresswith the receiver's ENS name - API generates a fresh one-time address
- An in-memory map
stealthAddress → recipientAddressis kept server-side - The EIP-681 URI sent in the QR encodes the stealth address as the transfer target
- The payer's wallet sends USDC to the stealth address
- Only the server can link the stealth address back to the recipient
No identity is ever published on-chain. The payer sees only a one-time address.
Note: The current stealth store is in-memory (
stealthStore.ts). For production, persist the mapping to an encrypted database.
Cross-chain payments use Across Protocol v3:
getBridgeQuote()callsapp.across.to/api/suggested-feeswith payer chain, destination chain, token, and amount- The API returns a
spokePoolAddressdynamically — no hardcoded SpokePool addresses - The frontend calls
SpokePool.depositV3()on the payer's chain - Across relayers deliver USDC to the receiver on the destination chain
Testnet limitation: Across Protocol only supports mainnet chains. On testnets, cross-chain attempts show a descriptive error. The chain mapping and quote logic are fully wired for mainnet.
- Fork the repo
- Create a feature branch:
git checkout -b feat/your-feature - Commit with conventional commits:
feat:,fix:,docs:,refactor: - Open a pull request against
main
Please keep PRs focused — one feature or fix per PR.
MIT © CodeswithrohStudio