Splitsy is a Web3 bill-splitting dApp (decentralized application) built on the Internet Computer Protocol (ICP) blockchain. Users authenticate with their Internet Identity (ICP's native, passwordless identity system), create a shared bill, invite participants, and record payments — all on-chain with no central server or database.
- How It Works
- Tech Stack
- Project Structure
- Prerequisites
- Local Development Setup
- Backend Canister API
- Known Bugs & Issues
- Deployment to ICP Mainnet
- Sign In — The user authenticates via Internet Identity (ICP's decentralized identity provider). No username or password is stored anywhere.
- Create a Bill — Enter a total amount and a comma-separated list of participant names. The backend canister calculates an equal split and stores the bill on-chain.
- Pay a Bill — A participant enters their name and the amount they are paying. The canister records the payment against the active bill.
- Check Status — Query the canister to see who has paid and who still has a pending balance.
All state (the active bill and all payments) lives in a Rust canister deployed on ICP — not in a traditional database or cloud service.
| Layer | Technology |
|---|---|
| Blockchain | Internet Computer Protocol (ICP) |
| Backend Canister | Rust (ic-cdk 0.17, candid 0.10) |
| Identity | Internet Identity (pulled canister rdmx6-jaaaa-aaaaa-aaadq-cai) |
| Frontend | React 18, Vite, React Router v7 |
| ICP SDK | DFX (Dfinity SDK) |
| Styling | Plain CSS + inline styles (Tailwind classes partially used) |
| Auth Client | @dfinity/auth-client ^2.3.0 |
BillSplitter/
├── dfx.json # DFX config — defines all canisters
├── Cargo.toml # Rust workspace
├── package.json # Root npm workspace
├── deps/ # Pulled Internet Identity canister
│ ├── pulled.json
│ └── candid/rdmx6-jaaaa-aaaaa-aaadq-cai.did
└── src/
├── hackproj_backend/ # Rust canister (on-chain logic)
│ ├── Cargo.toml
│ ├── hackproj_backend.did # Candid interface
│ └── src/lib.rs # All backend logic
└── hackproj_frontend/ # React frontend
├── vite.config.js
├── index.html
└── src/
├── main.jsx
├── App.jsx # Routing + Internet Identity login
├── index.scss
└── components/
├── LandingPage.jsx # Unauthenticated home / sign-in prompt
├── BillManagement.jsx # Main app UI (create, pay, status, wallet)
├── Dashboard.jsx # Alternate/legacy dashboard UI
├── CreateBill.jsx # Unused duplicate component
├── LoginPage.jsx # Unused legacy username/password form
├── navbar.jsx # Top navigation bar
└── navbar.css
- DFX SDK —
dfxCLI - Rust with the
wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown
- Node.js >= 16 and npm >= 7
# 1. Install frontend dependencies
npm install
# 2. Pull the Internet Identity canister dependency
dfx deps pull
# 3. Start the local ICP replica in the background
dfx start --background --clean
# 4. Initialize the pulled Internet Identity canister
dfx deps init
# 5. Deploy all canisters (backend + frontend + Internet Identity)
dfx deploy
# 6. Start the frontend dev server
npm startOnce deployed, visit the URL printed by dfx deploy (usually http://localhost:4943?canisterId=<frontend_canister_id>).
To regenerate the Candid JS bindings after backend changes:
npm run generateDefined in src/hackproj_backend/hackproj_backend.did and implemented in src/hackproj_backend/src/lib.rs.
| Function | Type | Signature | Description |
|---|---|---|---|
create_bill |
update |
(nat64, vec text) -> (text) |
Creates a new active bill with a total amount and list of participant names. Splits evenly. |
pay_bill |
update |
(text) -> (text) |
Records a payment for the named user against the active bill. |
check_status |
query |
() -> (text) |
Returns which participants still have unpaid balances. |
Note: The canister holds only one active bill at a time. Creating a new bill overwrites the previous one.
See the fixes needed section below for a full list. A summary:
- The
.didfile declarespay_billas taking onetextargument, but the Rust implementation accepts(text, nat64)— this mismatch will cause deployment errors. BillManagement.jsxcallsbackActor.pay_bill(user, amount, receiver)with three arguments, but the canister only accepts two.BillManagement.jsxcallsbackActor.create_billandbackActor.pay_billwithoutawait, so responses are never actually used.- The "Check Status" button in
BillManagement.jsxchecks the local wallet balance instead of callingbackActor.check_status(). - The wallet top-up and wallet balance are entirely local state — they are not connected to any on-chain token or ICP ledger.
- The
App.jsxrouting is inverted: authenticated users are sent to the landing page and unauthenticated users are redirected to/billmanagement. - The Navbar's
isLoggedInstate is alwaysfalse— it is never updated after a successful Internet Identity login. LoginPage.jsxandCreateBill.jsx(exported asBillManagement) are dead code — they are never used in routing.CreateBill.jsxhas a duplicatemessagekey in itsstylesobject, which silently discards one of the values.BillManagement.jsxpasses areceiverfield topay_billthat the canister does not support.
# Make sure you have ICP cycles in your wallet, then:
dfx deploy --network icThe internet_identity canister does not need to be deployed by you — it already exists on mainnet at rdmx6-jaaaa-aaaaa-aaadq-cai. DFX handles this automatically via the pull type in dfx.json.