jerico

Jerico: Instant blockchain invoicing with PYUSD, QR payments & global transfers.

jerico

Created At

ETHGlobal New Delhi

Project Description

Jerico β€” Full technical & product deep-dive πŸš€

Below is a thorough, structured description of Jerico: what it is, how it works, architecture choices, edge cases, implementation suggestions, and next steps. I’ve included concrete examples, code patterns, data models, and trade-offs so you (or judges/engineers) can act on it immediately.

  1. Executive summary

Jerico is a mobile-first, blockchain-native invoicing + payment platform that uses on-chain smart contracts and PYUSD to enable instant, peer-to-peer invoice settlement with zero platform custody. Freelancers and businesses create invoices (on-chain or hybrid), share links/QR codes, and clients approve & pay directly in PYUSD. The system provides real-time payment visibility, global accessibility, gas- and storage-optimized contracts, and can reduce payment costs by up to ~90% vs legacy rails.

  1. Core value proposition (concise)

Instant settlement on approval (no multi-day bank delays).

Very low fees (direct token transfer).

Transparent, auditable ledger of invoice lifecycle.

Mobile-first UX: QR & deep links for one-tap payments.

Zero custodial risk β€” platform never holds funds.

  1. High-level user flows (step-by-step)

A. Invoice creation (issuer / freelancer)

Fill invoice form (client, items, total, due date, metadata, optional payer address).

Optionally upload full invoice PDF/metadata to IPFS/Arweave; store CID or hash.

Submit β†’ a transaction to InvoiceFactory.createInvoice(...) OR server stores a signed invoice and pins metadata to IPFS then pushes an on-chain record (hybrid).

User gets invoice ID, shareable payment link and QR code.

B. Client payment (payer)

Client scans QR or clicks payment link β†’ opens DApp/wallet.

DApp shows invoice details, token (PYUSD), exact on-chain amount (smallest units).

Client approves allowance (ERC20 approve) or uses permit (EIP-2612) to allow contract to transfer.

Client triggers payInvoice(invoiceId) which performs an atomic safeTransferFrom(payer, issuer, amount).

Contract emits InvoicePaid event β†’ frontends and backend listeners update status to Paid.

C. Visibility & notifications

Backend/event indexer (or The Graph) listens to events and pushes websocket/SSE/Push notifications and updates dashboards in real time.

  1. Smart contract architecture (recommended) Components

InvoiceFactory (main contract / registry)

Creates invoice records (stores minimal on-chain data + metadata hash/CID).

Emits events: InvoiceCreated, InvoicePaid, InvoiceCanceled, InvoiceExpired.

Facilitates payInvoice by calling IERC20(token).safeTransferFrom(payer, issuer, amount) β€” this moves tokens directly from payer to issuer (no platform custody).

(Optional) InvoiceProxy β€” if you want per-invoice contract objects (more expensive; generally unnecessary).

Use OpenZeppelin libraries (SafeERC20, ReentrancyGuard, Ownable) and uint packing to save gas.

Invoice struct (gas-optimized example)

Store minimal fields on-chain to reduce gas; put full details off-chain (IPFS).

struct Invoice { address issuer; // 20 bytes address payer; // 20 bytes (optional = address(0) for any payer) address token; // 20 bytes (PYUSD contract) uint96 amount; // smallest units (sufficient for typical invoice amounts) uint32 dueDate; // unix timestamp uint32 createdAt; // unix timestamp uint8 status; // enum: 0=Pending,1=Paid,2=Expired,3=Canceled bytes32 metadataCID; // IPFS CID hash or keccak256(metadata) (optional) }

Invoice ID

Use bytes32 invoiceId = keccak256(abi.encodePacked(issuer, payer, token, amount, dueDate, nonce));

Store mapping mapping(bytes32 => Invoice) invoices;

Emit InvoiceCreated(invoiceId, ...) so indexers can find it.

Events

event InvoiceCreated(bytes32 indexed invoiceId, address indexed issuer, address indexed payer, address token, uint256 amount, uint256 dueDate);

event InvoicePaid(bytes32 indexed invoiceId, address indexed payer, uint256 amount, uint256 paidAt);

event InvoiceCanceled(bytes32 indexed invoiceId, address indexed issuer);

event InvoiceExpired(bytes32 indexed invoiceId);

  1. Payment mechanics & zero-custody detail

safeTransferFrom(payer, issuer, amount): Contract initiates transfer; token contract debits payer and credits issuer. Token never sits in a Jerico-controlled account β€” no custody.

Allowance requirement: payer must approve InvoiceFactory for amount. To reduce friction, support:

EIP-2612 permits (signatures + permit) β€” single-tx approval + pay.

Meta-transactions / relayers that pay gas on behalf of payer (for UX), optionally with gas sponsorship.

Edge tokens: For fee-on-transfer tokens, verify recipient’s received delta or disallow such tokens if exact amount required.

  1. PYUSD specifics (precision handling)

PYUSD has 6 decimals (unlike ETH with 18).

On-chain amounts are in smallest units where 1 PYUSD = 10⁢ units.

Example: 12.345 PYUSD β†’ 12.345 * 10^6 = 12,345,000 (on-chain integer).

Example: 123.45 PYUSD β†’ 123.45 * 10^6 = 123,450,000.

Frontend must use ethers.utils.parseUnits(amountString, 6) to convert user input to an on-chain BigNumber.

  1. Frontend architecture & UX (React 19 + TypeScript)

Stack: React 19 + TypeScript, Vite, TailwindCSS, RainbowKit + Wagmi, Framer Motion.

Wallet flow:

Use Wagmi connectors: Injected (MetaMask), WalletConnect, Coinbase Wallet.

Detect network; prompt chain-switch if needed (Sepolia for demo, Mainnet for production).

Payment page responsibilities:

Show issuer info, line items (pull from IPFS if off-chain), due date, and exact PYUSD amount.

Show payer balance and allowance; provide a one-click Approve & Pay path using permit if token supports it (or show two-step Approve then Pay).

QR code generation: generate link https://app.jerico/pay?invoiceId=<id>&network=ethereum and create QR; use qrcode.react or qrcode libs.

Deep-link for mobile wallets: walletconnect deeplink or metamask://dapp/… fallback.

  1. Backend, indexing & real-time updates

Indexer: Listen to chain events (Alchemy/Infura/websocket provider). On InvoiceCreated/InvoicePaid, index to MongoDB for fast search and dashboards.

Subgraph: Offer a Graph Protocol subgraph for powerful querying and history.

Real-time transport: Use WebSockets / SSE / Firebase / Pusher to notify frontends.

Storage:

On-chain: minimal invoice record and events.

Off-chain: invoice PDFs, metadata, line items (IPFS/Arweave + DB for indexing).

APIs (example):

GET /invoices?org=...&status=...&page=...

GET /invoice/:invoiceId

POST /invoice (if you support off-chain creation + signed anchoring)

POST /webhook/chain (for internal listeners)

  1. Security & best practices

Smart contract:

Use ReentrancyGuard for payment paths.

Use SafeERC20.safeTransferFrom.

Follow checks-effects-interactions pattern.

Validate dueDate > block.timestamp and amount > 0.

Emit events for all state transitions.

Limit metadata size stored on-chain; store large files off-chain.

Frontend:

Validate monetary inputs with precise decimal handling.

Protect against phishing: canonical DApp domain, verify invoice signer (optional).

Operational:

Monitor for suspicious invoice creation patterns (fraud).

Maintain immutable logs and retain IPFS CIDs.

Audits: Formal smart contract audit (recommended) before mainnet.

  1. Gas optimization & cost control

Store minimal fields on-chain; use bytes32 metadata hashes instead of strings.

Pack storage variables to consume fewer slots.

Emit events with rich data instead of writing large structs to storage.

Use uint96/uint128 for amounts instead of uint256 where safe.

Consider meta-transactions to move gas burden away from payer or issuer depending on UX decisions.

  1. Edge cases & business rules

Partial payments: Decide whether to allow partial payments or require full settlement.

Overpayments: Refund logic or auto-credit feature β€” must handle safely.

Expired invoices: either allow late payments with penalty or reject; status Expired.

Disputed invoices: off-chain dispute resolution flows and ability to lock pay action during dispute.

Transfer-fee tokens: detect tokens that reduce amount upon transfer; either disallow or adjust expected amount & show warning to payer.

Cancellation: issuer can cancel only if not yet paid.

  1. UX & mobile specifics (QR / deep linking)

QR payload: https://app.jerico/pay?invoiceId=<id>&network=mainnet

WalletConnect flow: open dApp in mobile wallet with the invoice page; use wagmi + rainbowkit to handle connect states.

PWA support: add to homescreen, offline invoice viewing.

One-tap share: share invoice link via SMS/WhatsApp/email.

  1. Observability, analytics & KPIs

Track: time-to-settlement, % on-time payments, total settlement volume (PYUSD), user growth, average fee saved (vs typical card or processor fees).

Dashboards: total outstanding invoices, paid/expired counts, per-customer volume.

Alerts: failed payments, contract upgrade attempts, unusual spikes.

  1. Deployment, testing & CI/CD

Smart contracts: develop with Hardhat or Foundry.

Unit tests, integration tests, fuzz tests, gas reports.

Frontend: Vite + TypeScript; test with Playwright / Cypress for flows (connect wallet, approve + pay via local signer).

CI: run tests, static analysis (Slither), gas usage reports, and pipelines to deploy to Sepolia then mainnet on approval.

Demo environment: Sepolia testnet + seeded PYUSD test tokens for hackathon/demo.

  1. Compliance & legal considerations

PYUSD is a stablecoin; legal/regulatory obligations vary by jurisdiction. Consider:

Anti-Money Laundering (AML) / KYC for higher volumes.

Terms of service for disputes/refunds.

Local money-transmission laws β€” consult counsel.

  1. Example data models

Invoice (off-chain indexed record)

{ "invoiceId": "0xabc123...", "issuer": "0xIssuerAddress", "payer": "0xPayerAddress", "token": "0xPYUSDAddress", "amount": "123450000", // in PYUSD smallest unit (6 decimals) "amountHuman": "123.45", "dueDate": 1735689600, "status": "Pending", "metadataCID": "Qm... (IPFS)", "createdAt": 1695861234 }

  1. Minimal smart contract snippet (illustrative) // SPDX-License-Identifier: MIT pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract InvoiceFactory is ReentrancyGuard { using SafeERC20 for IERC20;

enum Status { Pending, Paid, Expired, Canceled }

struct Invoice { address issuer; address payer; address token; uint96 amount; // smallest units (PYUSD: 6 decimals) uint32 dueDate; uint32 createdAt; uint8 status; bytes32 metadata; // optional }

mapping(bytes32 => Invoice) public invoices;

event InvoiceCreated(bytes32 indexed id, address issuer, address payer, address token, uint256 amount, uint256 dueDate); event InvoicePaid(bytes32 indexed id, address payer, uint256 amount, uint256 paidAt); event InvoiceCanceled(bytes32 indexed id);

function createInvoice(address payer, address token, uint96 amount, uint32 dueDate, bytes32 metadata) external returns (bytes32) { require(amount > 0, "zero amount"); require(dueDate > block.timestamp, "invalid dueDate"); bytes32 id = keccak256(abi.encodePacked(msg.sender, payer, token, amount, dueDate, block.timestamp)); require(invoices[id].issuer == address(0), "exists"); invoices[id] = Invoice(msg.sender, payer, token, amount, dueDate, uint32(block.timestamp), uint8(Status.Pending), metadata); emit InvoiceCreated(id, msg.sender, payer, token, amount, dueDate); return id; }

function payInvoice(bytes32 id) external nonReentrant { Invoice storage inv = invoices[id]; require(inv.issuer != address(0), "not found"); require(inv.status == uint8(Status.Pending), "not pending"); require(block.timestamp <= inv.dueDate, "expired"); if (inv.payer != address(0)) require(msg.sender == inv.payer, "not payer");

inv.status = uint8(Status.Paid);
IERC20(inv.token).safeTransferFrom(msg.sender, inv.issuer, inv.amount);

emit InvoicePaid(id, msg.sender, inv.amount, block.timestamp);

}

function cancelInvoice(bytes32 id) external { Invoice storage inv = invoices[id]; require(inv.issuer == msg.sender, "only issuer"); require(inv.status == uint8(Status.Pending), "not pending"); inv.status = uint8(Status.Canceled); emit InvoiceCanceled(id); } }

(This is illustrative β€” production needs tests, fee/edge token handling, permit support, and audit.)

  1. Integrations & ecosystem (recommended)

Wallets: MetaMask, Rainbow, Coinbase, WalletConnect.

Providers: Alchemy, Infura, QuickNode for event websockets.

Indexing: The Graph or custom indexer with MongoDB.

Storage: IPFS / Arweave for invoices & attachments.

Analytics/notifications: Pusher/Firebase/Sentry for errors and notifications.

Accounting: Integrations with QuickBooks/Xero (webhooks / CSV export).

  1. Roadmap & feature ideas

Short term: Sepolia demo, permit approvals, QR + link UX polish, The Graph subgraph.

Medium term: meta-transactions/gas sponsorship, partial payments, refunds, batch invoicing.

Long term: FIAT on/off ramps, recurring invoices, multi-sig / escrow support, advanced dispute resolution.

  1. Hackathon/demo checklist (quick)

Deployed InvoiceFactory on Sepolia.

Simple React demo: create invoice, show QR, pay from another wallet.

Real-time front-end update via websockets after InvoicePaid.

Metrics panel: volume, average settlement time, cost saved example.

Clear README + short video walkthrough.

  1. Suggested KPIs to highlight in pitch

Settlement time (seconds vs days).

Average cost saved per transaction (%) β€” compute example vs 3% card fee.

USD-volume settled on-chain in demo.

Number of invoices processed in demo (scale tests).

  1. Final implementation notes & trade-offs

All on-chain vs hybrid: storing full invoice on-chain is simple but expensive. Hybrid (hash/CID on-chain + full data on IPFS + indexer for search) is recommended for cost efficiency.

Permit vs Approve UX: permit reduces an extra approval tx but requires token support. For PYUSD check if permit is supported β€” otherwise two-step approve remains necessary.

Gasless UX: meta-transactions improve new-user onboarding but add infrastructure and cost for relayer sponsorship.

Regulation: operating a global payments platform may expose you to AML/KYC obligations β€” get legal review.

How it's Made

πŸ—οΈ Architecture & Technology Integration 🎯 Core Tech Stack

βš›οΈ Frontend: React 19 + TypeScript, Vite 7.x, TailwindCSS 4.x, Framer Motion

🌐 Web3 Stack: Wagmi 2.x, Viem 2.x, RainbowKit 2.2.8, TanStack Query

🧠 Custom Hook Layer:

useInvoiceContract() β†’ write ops

useInvoiceDetails() β†’ read invoice

useUserInvoices() β†’ batch fetch

usePYUSDContract() β†’ approvals & balances

usePaymentValidation() β†’ unified pre-check (balance, allowance, approval)

πŸ”₯ Key Technical Highlights

πŸ’° PYUSD Decimals Handling β†’ 6 decimals (vs 18 standard)

export function parsePYUSDAmount(amount: string): bigint { return parseUnits(amount, 6); }

πŸ” Invoice ID Generation β†’ Keccak256 over multiple params + block.timestamp & block.number

πŸ“± Responsive Animations β†’ Window width tracking + dynamic padding

⚑ Real-time Polling β†’ Payment status (5s), balances (10s), invoices (on tx confirm)

🎭 Complex Navbar Animation β†’ Two-stage (drop-in β†’ expand)

πŸ“‹ Clipboard API w/ Fallback β†’ Works even on older browsers

πŸ”„ Approval State Machine β†’ Balance check β†’ Approve β†’ Pay β†’ Confirm

πŸš€ Creative Solutions

πŸ“Š TanStack Query + Wagmi Sync β†’ Auto refetch balances, invoices & status post-payment

πŸ”— ABI Management β†’ Single source of truth + TypeScript inference

πŸ“± QR Code Integration β†’ Mobile wallet payments via scan

🌈 RainbowKit Benefits:

Zero-config wallet support (20+)

Mobile-optimized QR connect

Theming with our color palette

⚑ Why Viem over web3.js?

Better TS inference

~50% smaller bundle

Native async/await

πŸ”§ Hacky but Effective

🎨 Dynamic CSS-in-JS Animations β†’ Responsive padding w/ Framer Motion

πŸ“ BigInt Token Math β†’ Safe handling of token precision

⏰ Unix Timestamp Conversion β†’ Milliseconds ↔ Seconds pitfalls solved

πŸ“¦ Performance Wins

πŸ”„ Intelligent Polling Strategy

Payments: 5s

Balances: 10s

Invoices: on confirmation

πŸš€ Code Splitting β†’ Vite + React Router lazy() imports

βœ… Hackathon Result

Built a production-ready Web3 invoice system within hackathon constraints:

🧩 Coordinated blockchain state ↔ UI updates in real-time

πŸ”’ Maintained full TypeScript type safety

⚑ Optimized UX with aggressive caching, smooth animations, and responsive design

background image mobile

Join the mailing list

Get the latest news and updates