Decentralized expense splitting on Stellar Testnet.
Create expenses, split by equal/percentage/weight, and settle shares
with real XLM transfers and verifiable transaction hashes.
Stellar-star solves the common "IOU but no payment" problem in group expense apps.
Most split apps only track debts. Stellar-star closes the loop by letting members settle instantly with XLM and verify results on-chain.
Every payment can be traced through an explorer transaction hash, and settlement metadata is stored via Soroban contract calls for transparency and dispute resistance.
| Property | Description |
|---|---|
| π Non-custodial | Users sign with their own wallet |
| π On-chain verifiable | Each payment has a real tx hash |
| πΌ Multi-wallet UX | Freighter, xBull, Lobstr support |
| β‘ Realtime sync | Supabase updates shared state across participants |
| Feature | Status |
|---|---|
| Multi-wallet connect (Freighter, xBull, Lobstr) | β Live |
| Expense split modes (equal, percentage, weighted/custom) | β Live |
| Per-share XLM settlement flow | β Live |
Soroban duplicate-settlement checks (is_paid) |
β Live |
On-chain payment recording (record_payment) |
β Live |
| Transaction hash receipt links | β Live |
| SEP-0007 QR generation | β Live |
| Trip net-balance optimization | β Live |
| Realtime sync (Supabase + contract events) | β Live |
| Responsive mobile-first UI | β Live |
| Duplicate wallet address validation (trip and expense forms) | β Live |
| Layer | Technology |
|---|---|
| App Framework | Next.js 15 (App Router) + TypeScript |
| UI | Tailwind CSS, Framer Motion, Radix UI |
| Blockchain | @stellar/stellar-sdk, Horizon, Soroban RPC |
| Smart Contract | Rust + soroban-sdk |
| Data Sync | Supabase (PostgreSQL + Realtime) |
| Testing | Jest + ts-jest + React Testing Library |
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ βββββββββββββββββ
β Connect βββββΆβ Create βββββΆβ Choose Split βββββΆβ Calculate β
β Wallet β β Expense β β Mode β β Shares β
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ βββββββββ¬ββββββββ
β
ββββββββββββββββ ββββββββββββββββ βββββββββΌββββββββ
β Sync State ββββββ Record on ββββββ Build/Sign β
β via Events β β Soroban β β & Submit TX β
ββββββββββββββββ ββββββββββββββββ βββββββββββββββββ
| Step | Action |
|---|---|
| 1 | User connects wallet (Freighter / xBull / Lobstr) |
| 2 | Expense is created with split strategy and participant weights |
| 3 | App computes each member's share in XLM |
| 4 | Payment transaction is built client-side and signed in wallet |
| 5 | Signed envelope is submitted to Horizon |
| 6 | Contract read/write checks enforce no duplicate settlement |
| 7 | UI updates from tx hash receipts, event polling, and realtime sync |
Latest deployed settlement contract (this workspace session):
| Detail | Value |
|---|---|
| Contract ID | CBS2BJQ4ZC2ZSAZ5XS47BGC6Q7VTMJA4SE2PVHFXGXAZI5ES6H645WHO |
| Deploy Transaction | View on Stellar Expert |
| Contract Explorer | View Contract |
| Transaction | Link |
|---|---|
| Settlement deploy tx | View |
| Pool deploy tx | View |
Pool init tx (pool_ini) |
View |
Settlement init tx (stx_ini) |
View |
Inter-contract settlement proof (record_payment + internal pool withdraw) |
View |
record_payment(trip_id, expense_id, payer, member, amount, tx_hash)
get_payments(trip_id)
is_paid(expense_id, member)Stellar-star employs a pool contract architecture where member balances are tracked. When recording a payment on-chain, the settlement contract calls the pool contract to withdraw the member's share amount:
deposit(member, amount): Allows any member to deposit mock pool credits for themselves (requires member's signature).withdraw(from, amount): Withdraws credit from a member (requires member's signature).balance_of(member): Returns the current mock pool credit balance for a member.
- β Prevent duplicate settlement for same expense/member pair
- β
Persist immutable settlement evidence (
tx_hash) - β Return payment history by trip for reconciliation
| Error Code | Name | Description |
|---|---|---|
#1 |
InvalidAmount |
Amount is zero or negative |
#2 |
AlreadyPaid |
Duplicate settlement attempt |
#3 |
EmptyId |
Missing trip or expense identifier |
| Requirement | Evidence |
|---|---|
| Public repository | GitHub Repo |
| Live demo | stellar-star-five.vercel.app |
| Demo video | YouTube |
| Contract details and tx proof | Smart Contract section |
| UI screenshots | Screenshots section |
| Mobile screenshot proof | public/mobile-responsive.png |
| Test output screenshot | public/testcase.png |
| Release/runbook/proof docs | Documentation section |
| Requirement | Version |
|---|---|
| Node.js | 18+ |
| npm | 9+ |
| Rust toolchain | Latest (for contract work) |
| Stellar CLI | Latest (for contract deploy) |
| Freighter wallet | Set to Testnet |
# 1. Install dependencies
npm install
# 2. Start the dev server
npm run devOpen http://localhost:3000 in your browser.
First time setup?
# Copy the environment template
cp .env.local.example .env.localThen:
-
Add your Supabase URL, anon key and JWT secret to
.env.local -
Create the database schema β open the Supabase Dashboard, go to SQL Editor -> New Query, paste the whole of
supabase-setup.sqland hit Run. The script is idempotent, so it is safe to run again after any change. -
Verify the connection end to end:
npm run db:check
This checks the env vars, confirms
SUPABASE_JWT_SECRETreally signs this project's tokens, proves the tables exist, and exercises the whole sign-up write path (including that one wallet cannot read or take over another wallet's rows). It cleans up everything it creates. -
Ensure your wallet is on Stellar Testnet
Nothing loads after signing up? That is almost always step 2: without the tables, every query returns
PGRST205and the app falls back to an empty local cache.npm run db:checkwill tell you in one line.
| Layer | File | Responsibility |
|---|---|---|
| Schema, RLS, triggers | supabase-setup.sql |
Tables, policies, realtime, integrity triggers |
| Row types | types/supabase.ts |
Typed mirror of the SQL schema |
| Session | lib/supabase/session.ts |
Stores the wallet JWT; notifies React when it changes |
| Client | lib/supabase/client.ts |
One shared client that attaches the current token per request |
| Queries | lib/supabase/queries.ts |
Every read and write, plus row-to-domain mapping |
| Live data | lib/supabase/useRealtimeCollection.ts |
Fetch + realtime + per-wallet offline cache |
| Server | lib/supabase/server.ts |
Route-handler clients (never bundled for the browser) |
Authentication. There is no Supabase Auth user. /api/auth/challenge issues
a nonce, the wallet signs it, and /api/auth/verify checks the signature and
mints an HS256 JWT with a wallet_address claim, signed with the project's JWT
secret. Postgres verifies that token on every request, and every RLS policy
reads identity from it via public.current_wallet(). The same call also creates
or refreshes the user's profile, so sign-up completes in one round trip.
Access control. member_wallets is derived by a database trigger from the
members JSON, never sent by the client, so the array RLS filters on cannot
drift out of sync with the member list. created_by_wallet is frozen on update,
so a member can edit a shared trip or expense without taking ownership of it.
Use .env.local (or copy from .env.local.example):
# ββ Stellar Network ββββββββββββββββββββββββββββββ
NEXT_PUBLIC_STELLAR_NETWORK=TESTNET
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_STELLAR_EXPLORER=https://stellar.expert/explorer/testnet
# ββ Soroban / Smart Contract βββββββββββββββββββββ
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
# Deployed contract ID (example or placeholder)
NEXT_PUBLIC_CONTRACT_ID=CBS2BJQ4ZC2ZSAZ5XS47BGC6Q7VTMJA4SE2PVHFXGXAZI5ES6H645WHO
# ββ Supabase βββββββββββββββββββββββββββββββββββββ
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
# Supabase JWT secret used for server-side auth challenge signatures
SUPABASE_JWT_SECRET=your-supabase-jwt-secret-here
# ββ App Metadata βββββββββββββββββββββββββββββββββ
NEXT_PUBLIC_APP_NAME=Stellar-star
NEXT_PUBLIC_APP_VERSION=1.0.0
NEXT_PUBLIC_SITE_URL=http://localhost:3000Run all frontend/unit tests:
npm test -- --runInBandGenerate coverage report:
npm run test:coverageRun browser end-to-end tests (Playwright):
npm run test:e2e # headless, all projects (desktop + mobile viewports)
npm run test:e2e:ui # interactive UI mode
npx playwright test --project=chromium # single browser, fastest (what CI runs)Playwright starts the dev server itself and covers landing, auth prompt,
dashboard, expenses, trips, and trip detail pages plus two mobile-viewport
(Pixel 5 / iPhone 12) tests - see e2e/e2e.spec.ts. No real wallet or
Supabase project is needed; it asserts unauthenticated-state UI and
responsive layout. Full details in docs/RUNBOOK.md.
Verify proof assets & docs links (issue #73):
npm run proof:linksThis standalone script (script: scripts/proof-link-check.js) confirms that:
public/mobile-responsive.pngexists (the mobile viewport screenshot).README.mdreferences the local mobile proof asset.- All remote proof URLs in
README.md,docs/REQUIREMENT_PROOF_MATRIX.md,docs/ARCHITECTURE_AND_LIMITATIONS.md, anddocs/RELEASE_CHECKLIST.mdresolve successfully.
It is also wired into the quality job of .github/workflows/ci.yml, so every PR and push to main runs the same checks.
Current status in this workspace:
- Run
npm test -- --runInBandto see the latest total suites/tests after any new test cases are added. - Run
npm run lint,npx tsc --noEmit,npm run proof:links, andnpm run buildfor release checks. - Duplicate wallet address validation (trip and expense forms) is covered by
__tests__/split/calculator.test.ts,__tests__/hooks/useExpenseFormValidation.test.ts, and__tests__/components/trips/TripFormDuplicateWallet.test.tsx. - Pending on-chain retry persistence (localStorage, wallet-scoped, survives refresh) is covered by
__tests__/utils/pendingOnChain.test.tsand__tests__/payment/usePayment.retry-persistence.test.tsx.
For Rust contract checks:
cd contract
cargo check
# optional
cargo test| # | Command | Purpose |
|---|---|---|
| 1 | npm run lint |
Lint checks |
| 2 | npx tsc --noEmit |
Type checking |
| 3 | npm test -- --runInBand |
Run tests |
| 4 | npm run proof:links |
Verify mobile proof asset & docs links |
| 5 | npm run build |
Production build |
| 6 | cd contract && cargo check |
Rust contract check |
npm run build
npm run startScript:
bash scripts/deploy-contract.sh <stellar-cli-account-alias-or-secret> <token-contract-id>Example:
bash scripts/deploy-contract.sh stellar-star-deployer C... # Stellar Asset Contract IDThe script builds, deploys, and cross-initializes both the Stellar-star Settlement contract and the Settlement Pool contract on testnet automatically.
After deployment, update:
NEXT_PUBLIC_CONTRACT_IDandNEXT_PUBLIC_SETTLEMENT_CONTRACT_IDwith the printed settlement contract ID.NEXT_PUBLIC_POOL_CONTRACT_IDwith the printed pool contract ID.NEXT_PUBLIC_POOL_TOKEN_IDwith the token contract ID supplied to the script.
Notes:
- If the script is not executable in your shell, run it via
bash scripts/deploy-contract.sh <alias-or-secret> <token-contract-id>.- The script resolves the deployer's address to initialize both contract structures properly.
- Always verify the returned contract IDs on Stellar Expert explorer.
stellar-star/
β
βββ π app/ -> Next.js app routes
βββ π components/ -> UI and feature components
βββ π context/ -> React context providers
βββ π hooks/ -> App hooks (wallet, payment, events, etc.)
βββ π lib/ -> Utilities, Stellar integration, Supabase client
βββ π contract/ -> Soroban Rust smart contract
βββ π __tests__/ -> Jest test suites
βββ π docs/ -> Runbook, checklist, architecture, requirement matrix
βββ π scripts/ -> Deployment scripts
βββ π types/ -> Shared TypeScript types
| Document | Link |
|---|---|
| Release Checklist | RELEASE_CHECKLIST.md |
| Production Runbook | RUNBOOK.md |
| Requirement Proof Matrix | REQUIREMENT_PROOF_MATRIX.md |
| Architecture and Limitations | ARCHITECTURE_AND_LIMITATIONS.md |
MIT (2026) Stellar-star








