On-chain royalty distribution for NFT collaborators on Stellar.
A Soroban smart contract that automatically splits NFT sale proceeds
among multiple collaborators based on predefined percentage allocations —
instantly, on-chain, with no intermediaries.
Stellar Royalty Splitter solves the coordination problem in multi-collaborator NFT projects. Instead of relying on a central party to manually divide and send proceeds, the contract enforces the agreed split at the protocol level. Shares are defined once at initialization and cannot be altered — every distribution is deterministic, transparent, and verifiable on-chain.
The contract supports both primary sales and secondary market royalties, with rounding handled explicitly so the full amount is always distributed.
- How It Works
- Prerequisites
- Build
- Test
- Deploy
- Contract API
- Usage Examples
- Rounding
- Frontend & Backend
- Environment Variables
- Project Structure
- Roadmap
- Contributing
- License
Deploy contract
│
▼
initialize(collaborators, shares) ← one-time setup, basis points sum to 10,000
│
▼
NFT sale occurs → funds sent to contract address
│
▼
distribute(token, amount) ← splits and transfers proportionally, on-chain
│
▼
Each collaborator receives their share instantly
Shares are expressed in basis points (1 bp = 0.01%). They must sum to 10,000 (100%).
| Tool | Install |
|---|---|
| Rust | https://rustup.rs |
| wasm32 target | rustup target add wasm32-unknown-unknown |
| Stellar CLI | cargo install --locked stellar-cli |
cargo build --target wasm32-unknown-unknown --releasecargo testchmod +x scripts/deploy.sh
./scripts/deploy.shThe deploy script targets Stellar Testnet by default. See Environment Variables to switch to Mainnet.
Sets up the revenue split. Can only be called once. Subsequent calls will be rejected.
| Parameter | Description |
|---|---|
collaborators |
List of recipient wallet addresses |
shares |
Basis-point allocation per collaborator (must sum to 10,000) |
Transfers amount of token from the contract address to all collaborators proportionally. Any rounding dust is assigned to the last collaborator — see Rounding.
Records a secondary market resale and accumulates the royalty amount owed to collaborators.
Distributes all pending secondary royalties accumulated via record_secondary_sale.
Returns all registered collaborator addresses.
Returns the basis-point share for a given collaborator address.
# 50% artist / 30% musician / 20% animator
stellar contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- initialize \
--collaborators '["GARTIST...","GMUSICIAN...","GANIMATOR..."]' \
--shares '[5000,3000,2000]'# Distribute 1,000 XLM from a sale (amounts in stroops: 1 XLM = 10,000,000 stroops)
stellar contract invoke \
--id <CONTRACT_ID> \
--source seller \
--network testnet \
-- distribute \
--token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC \
--amount 10000000000stellar contract invoke \
--id <CONTRACT_ID> \
--source anyone \
--network testnet \
-- get_share \
--collaborator GARTIST...# Record a 5% royalty from a 500 XLM resale
stellar contract invoke \
--id <CONTRACT_ID> \
--source marketplace \
--network testnet \
-- record_secondary_sale \
--nft_id "NFT_001" \
--previous_owner GBUYER1... \
--new_owner GBUYER2... \
--sale_price 5000000000 \
--sale_token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSCstellar contract invoke \
--id <CONTRACT_ID> \
--source anyone \
--network testnet \
-- distribute_secondary_royalties \
--token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSCPayouts use integer division. Any rounding dust (typically 1–2 stroops) is assigned to the last collaborator in the list, ensuring the full distributed amount always equals the input — no funds are ever left in the contract.
A React frontend and Express backend are included for interacting with the contract via a UI.
# Backend
cd backend
cp .env.example .env # fill in your keys
npm install
npm run dev # → http://localhost:3001
# Frontend (separate terminal)
cd frontend
npm install
npm run dev # → http://localhost:5173The frontend proxies /api/* to the backend automatically via the Vite config.
The backend builds unsigned transaction XDR and returns it to the frontend. Freighter signs and submits client-side — your private key never leaves the browser.
Copy backend/.env.example to backend/.env:
cp backend/.env.example backend/.env| Variable | Description |
|---|---|
PORT |
Port the backend API listens on (default: 3001) |
STELLAR_NETWORK |
testnet or mainnet |
HORIZON_URL |
Horizon REST endpoint for the chosen network |
SOROBAN_RPC_URL |
Soroban RPC endpoint for simulating and preparing transactions |
SERVER_SECRET_KEY |
Server-side keypair used for read-only simulations only — never signs user transactions |
Stellar-Royalty-Splitter/
├── src/
│ └── lib.rs # Soroban smart contract (Rust)
├── tests/
│ └── integration_test.rs
├── scripts/
│ └── deploy.sh
├── Cargo.toml
├── frontend/ # React + Vite UI
│ └── src/
│ ├── App.tsx
│ ├── api.ts # Backend API client
│ └── components/
│ ├── WalletConnect.tsx # Freighter wallet connection
│ ├── InitializeForm.tsx # Collaborator setup
│ ├── DistributeForm.tsx # Trigger distribution
│ └── CollaboratorTable.tsx # View current splits
└── backend/ # Express API
└── src/
├── index.js
├── stellar.js # Soroban RPC helpers
└── routes/
├── initialize.js
├── distribute.js
└── collaborators.js
- Primary sale distribution
- Secondary market resale royalty hooks
- Dashboard UI for earnings tracking
- Admin authorization on
set_royalty_rateanddistribute - Dynamic royalty adjustments via governance
- Role-based contributor management
See CONTRIBUTING.md for setup instructions, branch naming conventions, and the PR checklist.