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.
Replaces the contract's executable WASM while preserving all instance storage (admin, collaborators, shares, balances, etc.). Requires admin authorization. The replacement Wasm must be uploaded to the network first via stellar contract upload; use the returned hash as wasm_hash.
# 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...# 1. Build and upload the new contract Wasm
cargo build --target wasm32-unknown-unknown --release
stellar contract upload \
--source deployer \
--wasm target/wasm32-unknown-unknown/release/stellar_royalty_splitter.wasm \
--network testnet
# → aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2
# 2. Invoke update_wasm with the uploaded hash (admin must sign)
stellar contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- update_wasm \
--wasm_hash aa24c81289997ad815489b29db337b53f284cca5aba86e9a8ae5cef7d31842c2# 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 |
SIGNING_KEY_FILE |
Optional secrets-manager file path; takes precedence over SERVER_SECRET_KEY on load |
ADMIN_ROTATE_TOKEN |
Bearer token for POST /admin/rotate-key hot-reload without redeploy (#293) |
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.