On-chain Invoice Financing built on Stellar Soroban
SMEs tokenize unpaid invoices as NFTs and sell them at a discount to global liquidity providers — unlocking instant stablecoin liquidity without banks.
- Overview
- Live Demo
- Features
- Tech Stack
- Getting Started
- Project Structure
- Design System
- Environment Variables
- Core User Flows
- Smart Contract Integration
- Contributing
- Roadmap
- License
Kora Protocol is a decentralized invoice financing platform built on Stellar Soroban. It bridges the gap between SMEs in emerging markets who need working capital and global investors seeking yield on short-duration, real-world assets.
The problem: SMEs in Africa, Asia, and Latin America hold trillions of dollars in unpaid invoices. Traditional invoice financing is slow, expensive, and inaccessible to most small businesses.
The solution: Kora tokenizes invoices as NFTs on Stellar Soroban. Investors fund them via USDC. Settlement is instant, transparent, and non-custodial.
SME Kora Protocol Investor
│ │ │
├─ Upload Invoice ─────────► │
│ ├─ Store on IPFS │
│ ├─ Mint NFT on Soroban │
│ ├─ List on Marketplace ───►
│ │ ├─ Browse & Fund
│◄─ Receive USDC ──────────┤◄─ USDC Deposited ───────┤
│ │ │
│ (on due date) │ │
├─ Repay Principal ────────► │
│ ├─ Distribute Yield ──────►
│ │ │
Testnet deployment: https://kora-protocol.vercel.app (coming soon)
To run locally, see Getting Started.
- ✅ Connect Stellar wallet (Freighter, xBull, LOBSTR, Albedo)
- ✅ Upload invoice PDF to IPFS via Pinata
- ✅ Mint invoice as NFT on Soroban with one click
- ✅ Set custom discount rate and minimum investment
- ✅ Receive USDC instantly when invoice is funded
- ✅ Dashboard to track all active invoices and repayments
- ✅ Browse marketplace with filters (category, jurisdiction, risk tier, APR)
- ✅ View detailed invoice information and risk scores
- ✅ Fund invoices with USDC (partial or full)
- ✅ Real-time funding progress bars
- ✅ Portfolio dashboard with yield tracking
- ✅ Analytics with charts (portfolio growth, yield, risk distribution)
- ✅ Non-custodial — funds held in Soroban smart contract escrow
- ✅ On-chain risk scoring and repayment history
- ✅ IPFS-stored invoice metadata (tamper-proof)
- ✅ Transaction status toasts with hash links
- ✅ Optimistic UI updates
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript 5.6 |
| Styling | TailwindCSS 3.4 + CSS Variables |
| UI Components | Custom + Radix UI primitives |
| State Management | Zustand 5 |
| Data Fetching | TanStack Query v5 |
| Animations | Framer Motion 11 |
| Forms | React Hook Form + Zod |
| Charts | Recharts 2 |
| File Upload | React Dropzone |
| Notifications | Sonner |
| Blockchain | Stellar Soroban (via @stellar/stellar-sdk) |
| Wallet | Stellar Wallets Kit (@creit.tech/stellar-wallets-kit) |
| Storage | IPFS via Pinata |
The Kora frontend uses a semantic Tailwind-based design system with CSS custom properties in app/globals.css.
- Color tokens are defined in HSL and mapped through
tailwind.config.ts. - The app is dark-mode-first and supports light mode through theme overrides.
- Reusable primitives live under
components/ui.
Read the full design system documentation in DESIGN_SYSTEM.md.
- Node.js 18+ and npm/yarn/pnpm
- A Stellar wallet browser extension (Freighter recommended)
- A Pinata account for IPFS uploads (free tier works)
# 1. Clone the repository
git clone https://github.com/your-org/kora-frontend.git
cd kora-frontend
# 2. Install dependencies
npm install
# 3. Set up environment variables
cp .env.example .env.local
# Edit .env.local with your values (see Environment Variables section)
# 4. Start the development server
npm run devOpen http://localhost:3000 in your browser.
If you prefer Docker, run:
cp .env.example .env.local
# Edit .env.local with your values
docker compose upThe app will be available at http://localhost:3000 with hot reload enabled.
The app ships with mock data enabled by default (NEXT_PUBLIC_ENABLE_MOCK_DATA=true). You can browse the marketplace, view invoice details, and explore dashboards without a live Soroban connection.
To test wallet interactions, install Freighter, switch it to Testnet, and fund your account via Stellar Friendbot.
Once you have real contract IDs deployed to testnet (NEXT_PUBLIC_ENABLE_MOCK_DATA=false), scripts/seed-testnet.ts sets up a wallet with sample data to develop against:
npm run seed:testnetThis generates a new keypair, funds it via Friendbot, mints it testnet USDC, mints 5 sample invoices, partially funds 2 of them, and prints the wallet's public/secret key and the minted token IDs to the console — save the secret key if you want to reuse that wallet (e.g. to import it into Freighter).
Validate the script against your configured contracts without writing any invoice or funding state on-chain:
npm run seed:testnet -- --dry-run--dry-run still generates and funds a keypair (a real, funded account is required to simulate contract calls at all) and builds + simulates every mint call, but never signs or submits a mint or fund transaction.
The script refuses to run unless NEXT_PUBLIC_STELLAR_NETWORK=testnet — it will never touch mainnet. It reuses lib/stellar/contracts.ts and @stellar/stellar-sdk directly (no extra tooling needed) via Node's built-in TypeScript support, so it requires Node.js 22.6+.
kora-frontend/
├── app/ # Next.js App Router pages
│ ├── page.tsx # Landing page
│ ├── layout.tsx # Root layout + providers
│ ├── globals.css # Global styles + CSS variables
│ ├── providers.tsx # QueryClient, Toaster, WalletModal
│ ├── marketplace/
│ │ ├── page.tsx # Invoice marketplace listing
│ │ └── [id]/page.tsx # Invoice detail + fund panel
│ ├── invoice/
│ │ └── create/page.tsx # 3-step create invoice wizard
│ ├── dashboard/
│ │ ├── sme/page.tsx # SME dashboard
│ │ └── investor/page.tsx # Investor dashboard
│ └── analytics/page.tsx # Portfolio analytics + charts
│
├── components/
│ ├── ui/ # Reusable primitive components
│ │ ├── button.tsx
│ │ ├── card.tsx # Card + GlassCard
│ │ ├── badge.tsx
│ │ ├── skeleton.tsx
│ │ ├── progress.tsx
│ │ ├── input.tsx
│ │ ├── dialog.tsx
│ │ ├── select.tsx
│ │ └── stat-card.tsx
│ ├── invoice/
│ │ └── InvoiceCard.tsx # Marketplace invoice card
│ ├── wallet/
│ │ ├── WalletConnectModal.tsx
│ │ └── WalletButton.tsx
│ └── layout/
│ └── Navbar.tsx
│
├── hooks/
│ ├── useWallet.ts # Stellar Wallets Kit wrapper
│ ├── useTransaction.ts # Build → sign → submit lifecycle
│ └── useInvoices.ts # TanStack Query invoice hooks
│
├── lib/
│ ├── stellar/
│ │ ├── client.ts # Soroban RPC + Horizon client
│ │ ├── contracts.ts # Contract call builders
│ │ └── index.ts
│ ├── ipfs.ts # Pinata upload helpers
│ ├── utils.ts # cn(), formatCurrency, etc.
│ └── validations/
│ └── invoice.ts # Zod schemas
│
├── services/
│ ├── mockData.ts # Mock invoices + stats
│ └── invoiceService.ts # Invoice CRUD + contract calls
│
├── store/
│ ├── walletStore.ts # Wallet state (persisted)
│ ├── invoiceStore.ts # Marketplace filters + sort
│ ├── uiStore.ts # Modal + tx state
│ └── index.ts
│
├── types/
│ ├── invoice.ts # Invoice, InvoiceMetadata, etc.
│ ├── user.ts # WalletState, UserProfile, etc.
│ ├── contract.ts # ContractConfig, TxState, etc.
│ └── index.ts
│
├── .env.example # Environment variable template
├── next.config.js
├── tailwind.config.ts
├── tsconfig.json
└── package.json
Copy .env.example to .env.local and fill in the values:
# Stellar Network
NEXT_PUBLIC_STELLAR_NETWORK=testnet
NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
# Contract Addresses — v0.2 testnet deployments (replace with your own if needed)
NEXT_PUBLIC_INVOICE_CONTRACT_ID=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
NEXT_PUBLIC_MARKETPLACE_CONTRACT_ID=CBWOAOZCOAJQH7HHZRE5BVNL2C4HRP4JCQZF3YQCQYDL5BZJRN4YGK4
NEXT_PUBLIC_TOKEN_CONTRACT_ID=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# IPFS (Pinata)
NEXT_PUBLIC_IPFS_GATEWAY=https://gateway.pinata.cloud/ipfs
PINATA_JWT=your_pinata_jwt_token
# Feature Flags
NEXT_PUBLIC_ENABLE_MOCK_DATA=true # Set to false for live on-chain calls
NEXT_PUBLIC_ENABLE_DEVTOOLS=true
NEXT_PUBLIC_ENABLE_COMPARISON=true # Invoice comparison bar (marketplace); share via ?compare=id1,id2
NEXT_PUBLIC_ENABLE_ONBOARDING_TOUR=false
NEXT_PUBLIC_ENABLE_BATCH_ACTIONS=falseInvoice comparison: When
NEXT_PUBLIC_ENABLE_COMPARISON=true, marketplace cards show Add to Compare. Select up to 4 invoices, open the comparison table, and share the URL (?compare=…) to restore the selection. Seelib/featureFlags.tsandlib/comparison.ts.
- Connect wallet via Connect Wallet button
- Navigate to Create Invoice
- Fill in invoice details (debtor, amount, due date, jurisdiction)
- Set discount rate and minimum investment
- Upload invoice PDF
- Click Mint Invoice NFT — this:
- Uploads PDF to IPFS via Pinata
- Uploads metadata JSON to IPFS
- Builds a Soroban
mint_invoicetransaction - Prompts wallet for signature
- Submits to Stellar network
- Invoice appears on marketplace
- As investors fund it, USDC flows to your wallet
- Connect wallet
- Browse Marketplace — filter by APR, risk tier, jurisdiction
- Click an invoice card to view details
- Enter investment amount (respects min/max)
- Review expected return
- Click Fund Invoice — this:
- Builds a Soroban
fund_invoicetransaction - Prompts wallet for signature
- Submits to Stellar network
- Builds a Soroban
- Position appears in Investor Dashboard
- On repayment date, principal + yield is returned
The frontend interacts with two Soroban contracts:
| Contract | Address | Explorer |
|---|---|---|
| Invoice NFT | CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA |
View |
| Marketplace | CBWOAOZCOAJQH7HHZRE5BVNL2C4HRP4JCQZF3YQCQYDL5BZJRN4YGK4 |
View |
| USDC Token | CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
View |
To deploy your own contracts to testnet:
- Stellar CLI v21+
- Funded testnet account — get XLM from Friendbot
# 1. Install Stellar CLI
cargo install --locked stellar-cli --features opt
# 2. Generate or import a deployer keypair
stellar keys generate --global deployer --network testnet
stellar keys fund deployer --network testnet # fund via Friendbot
# 3. Build contract WASM (from the contracts repo)
stellar contract build
# 4. Deploy the Invoice NFT contract
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/invoice_nft.wasm \
--source deployer \
--network testnet
# → Outputs: CONTRACT_ID (copy this to NEXT_PUBLIC_INVOICE_CONTRACT_ID)
# 5. Deploy the Marketplace contract
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/marketplace.wasm \
--source deployer \
--network testnet
# → Outputs: CONTRACT_ID (copy this to NEXT_PUBLIC_MARKETPLACE_CONTRACT_ID)
# 6. Deploy or note the USDC token contract
# On testnet you can use the Stellar Lab USDC contract or deploy a test token.
# Copy its contract ID to NEXT_PUBLIC_TOKEN_CONTRACT_ID.
# 7. Update .env.local with the three contract IDs, then:
NEXT_PUBLIC_ENABLE_MOCK_DATA=false npm run devNote: The v0.2 testnet addresses in
.env.exampleare the canonical deployments. Override them only if you need a private deployment for development/testing.
lib/stellar/contracts.ts maintains a NETWORK_CONTRACTS registry keyed by network name.
When NEXT_PUBLIC_STELLAR_NETWORK changes (e.g. testnet → mainnet), the correct
addresses are picked automatically — no code changes needed, only env vars.
To add mainnet addresses when ready, extend the registry:
// lib/stellar/contracts.ts
const NETWORK_CONTRACTS = {
testnet: { invoice: "CBIELTK...", marketplace: "CBWOAOZ...", token: "CDLZFC3..." },
mainnet: { invoice: "C...", marketplace: "C...", token: "C..." },
};| Method | Description |
|---|---|
mint_invoice(ipfs_cid, amount, financing_amount, discount_rate, due_date) |
Mints a new invoice NFT |
get_invoice(token_id) |
Reads invoice state |
update_status(token_id, status) |
Updates invoice status (owner only) |
| Method | Description |
|---|---|
fund_invoice(token_id, amount) |
Investor funds an invoice |
repay_invoice(token_id) |
SME repays; triggers yield distribution |
get_positions(investor) |
Returns all investor positions |
// 1. Build unsigned transaction
const unsignedXdr = await invoiceContract.mintInvoice(params, walletAddress);
// 2. Sign with wallet
const signedXdr = await walletKit.signTransaction(unsignedXdr, { ... });
// 3. Submit to Soroban RPC
const result = await rpc.sendTransaction(tx);
// 4. Poll for confirmation
const confirmed = await waitForTransaction(result.hash);See CONTRIBUTING.md for guidelines on how to contribute to this project.
- v0.2 — Live Soroban contract deployment on testnet
- v0.3 — KYC/KYB integration (Synaps or Fractal ID)
- v0.4 — Secondary market for invoice positions
- v0.5 — Risk oracle integration (on-chain credit scoring)
- v0.6 — Multi-currency support (EURC, native XLM)
- v1.0 — Mainnet launch
MIT © 2025 Kora Protocol Contributors