Onchain batch payroll and accounting for Web3 startups and DAOs. Upload a CSV, pay everyone in one USDC transaction, tag it onchain, export the ledger. Built on Morph.
- Node.js v18 or higher
- npm v9 or higher
- MetaMask browser extension
- Morph Hoodi configured in MetaMask (details below)
No
.envfile needed. The frontend runs fully in Demo Mode out of the box — no wallet, no contract, no real funds required. Contract deployment is only needed for live transactions.
git clone https://github.com/Dami904/NovaPay.git
cd NovaPay
npm installnpm run devOpen http://localhost:5173 in your browser.
npm run build
npm run preview # preview the production build locallyAdd the network manually in MetaMask:
| Field | Value |
|---|---|
| Network Name | Morph Hoodi |
| RPC URL | https://rpc-hoodi.morphl2.io |
| Chain ID | 2910 |
| Currency Symbol | ETH |
| Block Explorer | https://explorer-hoodi.morphl2.io |
Or click "Switch to Morph" in the app — it will prompt MetaMask to add it automatically.
The app ships in Demo Mode by default (no real contract needed — transactions are simulated). To connect a real deployed contract:
- Deploy
NovaPay.solto Morph Hoodi - Note the deployed contract address
- Note the USDC token address on Morph Hoodi
- Open
src/utils/contractABI.jsand update:
export const NOVAPAY_CONTRACT_ADDRESS = '0xYourNovaPayContractAddress'
export const USDC_CONTRACT_ADDRESS = '0xYourUSDCTokenAddress'Once these are non-zero addresses, Demo Mode turns off automatically and the app calls real contracts.
The frontend expects this Solidity interface (from contracts/PayFlow.sol):
function batchPayout(
address token, // ERC20 token address (USDC or USDT)
address[] calldata recipients,
uint256[] calldata amounts, // in token units (6 decimals for USDC/USDT)
string calldata label
) external;
event PayrollBatch(
address indexed sender,
string label,
uint256 recipientCount,
uint256 totalAmount,
uint256 timestamp
);The contract pulls tokens from the caller via transferFrom — the frontend approves the exact total before calling batchPayout.
Upload a .csv file with these columns:
| Column | Required | Description |
|---|---|---|
wallet_address |
Yes | Recipient's EVM address |
amount |
Yes | USDC amount (e.g. 3000) |
name |
No | Employee/contractor name |
Column headers are case-insensitive. Aliases accepted:
- Address:
wallet_address,address,wallet - Amount:
amount,usdc_amount,usdc - Name:
name,employee_name,employee
Sample CSV — download from the app via the "Sample CSV" button, or use:
wallet_address,name,amount
0x1234567890123456789012345678901234567890,Alice Chen,3000
0x2345678901234567890123456789012345678901,Bob Smith,2500
0x3456789012345678901234567890123456789012,Carol Diaz,2500| Route | Screen |
|---|---|
/ |
Connect Wallet |
/dashboard |
Dashboard — stats + recent activity |
/payroll/new |
New Payroll Run — CSV upload, preview, send |
/payroll/confirm |
Transaction Confirmation |
/history |
Payroll Ledger — history + CSV export |
When NOVAPAY_CONTRACT_ADDRESS is the zero address (default), the app runs in Demo Mode:
- Two mock payroll runs are pre-populated in history
- The "Send Payroll" button simulates a 2.5-second transaction
- A fake tx hash is generated
- No MetaMask approval prompts appear
- USDC balance shows 50,000 for demo purposes
- All history is saved to
localStorageand persists across reloads
A yellow "DEMO MODE" badge appears in the navbar whenever Demo Mode is active.
NovaPay/
├── index.html
├── vite.config.js
├── package.json
├── hardhat.config.cjs # Contract deploy config (Morph Hoodi)
├── contracts/
│ └── PayFlow.sol # Batch payout smart contract
├── scripts/
│ └── deploy.cjs # Hardhat deploy script
├── src/
│ ├── index.jsx # React entry point
│ ├── App.jsx # Router + layout
│ ├── App.css # All styles (dark theme)
│ ├── context/
│ │ └── Web3Context.jsx # Wallet connection, contract calls, history
│ ├── utils/
│ │ ├── contractABI.js # ABI + contract/network config
│ │ ├── csvParser.js # Parse + validate uploaded CSV
│ │ └── csvExporter.js # Export history to CSV/XLSX
│ ├── components/
│ │ └── Navbar.jsx # Top navigation bar
│ └── pages/
│ ├── ConnectWallet.jsx
│ ├── Dashboard.jsx
│ ├── NewPayrollRun.jsx
│ ├── TransactionConfirmation.jsx
│ └── PayrollHistory.jsx
└── README.md
Every push to main or dev, and every pull request to main, triggers a build check automatically. If the build fails, the PR is blocked. No extra setup needed — the workflow is at .github/workflows/ci.yml.
- Push this repo to GitHub
- Go to vercel.com → Add New Project → import your repo
- Vercel auto-detects Vite — leave all settings as-is, then click Deploy
That's it. From then on:
- Every push to
main→ production deploy - Every PR → preview deployment with a unique URL
vercel.jsonhandles SPA routing so direct links like/dashboarddon't 404
- React 18 + React Router 6
- ethers.js v6 — wallet connection, contract interaction
- PapaParse — CSV parsing and export
- Vite — build tool
- Morph Hoodi — L2 blockchain
- Zero backend, zero auth — wallet is identity
npm install fails — Make sure you're on Node.js v18 or higher. Run node -v to check. Download the LTS version from nodejs.org.
vite: command not found — You skipped npm install. Run it first from inside the NovaPay folder.
"MetaMask not installed" — Install the MetaMask browser extension and refresh the page.
Stuck on "Connecting…" — Check that MetaMask is unlocked and you approved the connection request. Try refreshing the page and connecting again.
Wrong network warning — Click the warning banner in the app or open MetaMask manually and switch to Morph Hoodi (Chain ID 2910). The app can add the network automatically.
Port 5173 already in use — Start the dev server on a different port:
npm run dev -- --port 3000CSV upload shows errors — Check that wallet_address is a valid 0x EVM address and amount is a positive number. Use the "Sample CSV" button in the app to download a working example.
Transaction fails — Ensure your USDC balance covers the total payout. In live mode the app approves the exact total before calling the contract.