Decentralized event ticketing on Stellar using Soroban smart contracts. Issues NFT-like tickets, prevents scalping via strict-receive payments, enforces resale price caps, and pays royalties to creators on secondary sales.
stellar-ticket-protocol/
├── contracts/ # Soroban smart contracts (Rust)
│ ├── ticket_nft/ # NFT ownership, mint, transfer, approve
│ ├── ticket_factory/ # Event creation, supply/price config
│ ├── marketplace/ # Listings, resale cap enforcement, payment splits
│ └── royalty_engine/ # Royalty calculation and distribution
├── backend/ # Express REST API (TypeScript)
│ └── src/
│ ├── index.ts # App entry point
│ ├── routes.ts # API routes
│ ├── store.ts # In-memory store (swap for Redis/DB in prod)
│ ├── anti-bot.ts # Rate limiting + wallet blacklist
│ └── types.ts # Shared types
├── frontend/ # Next.js 14 web app (TypeScript + Tailwind)
│ └── src/
│ ├── app/ # App Router pages
│ ├── components/ # EventCard, WalletButton
│ └── context/ # WalletContext (Freighter integration)
└── sdk/ # @stellar-ticket/sdk — JS client library
└── src/index.ts # StellarTicketClient, tx builders, utils
Primary sale: Buyer creates a trustline for the ticket asset, then submits a strict-receive payment. The contract mints the ticket and assigns ownership.
Secondary sale: Seller lists at a price ≤ original_price × resale_cap_bps / 10000. Buyer pays; the contract splits funds (seller payout + creator royalty) and transfers the NFT atomically.
Anti-scalping: Resale cap is enforced on-chain. Off-chain, the backend rate-limits wallet requests (10/min) and auto-blacklists wallets that sustain abuse.
Metaverse access: GET /api/verify/:address/:ticketId — returns ownership status for gating access to virtual events.
- Rust stable +
wasm32-unknown-unknowntarget - Node.js 18+
rustup target add wasm32-unknown-unknowncd contracts
# Run tests
cargo test
# Build for deployment (requires stellar-cli)
cargo build --target wasm32-unknown-unknown --releasecd backend
npm install
npm run build # compile TypeScript
npm test # run Jest tests
npm run dev # start dev server on :3001Environment variables (optional):
PORT=3001
cd frontend
npm install
npm run build # production build
npm test # run Jest + Testing Library tests
npm run dev # start dev server on :3000Set NEXT_PUBLIC_API_URL to point at the backend (defaults to http://localhost:3001/api).
cd sdk
npm install
npm run build # outputs to dist/
npm test| Contract | Responsibility |
|---|---|
ticket_nft |
Mint tickets, track ownership, transfer with approval |
ticket_factory |
Create events with supply, price, resale cap, royalty config |
marketplace |
List/buy tickets; enforces resale cap; splits payment on buy |
royalty_engine |
Calculate and distribute royalties to event creators |
Key invariants enforced on-chain:
- A ticket ID can only be minted once
- Resale price ≤
original_price × resale_cap_bps / 10000 - Royalty ≤ 30% (
royalty_bps ≤ 3000) - Resale cap ≥ 100% (
resale_cap_bps ≥ 10000)
| Method | Path | Description |
|---|---|---|
POST |
/api/events |
Create an event |
GET |
/api/events |
List all events |
GET |
/api/events/:id |
Get event by ID |
GET |
/api/wallets/:address/tickets |
Get tickets owned by address |
POST |
/api/listings |
List a ticket for resale |
GET |
/api/listings |
Get all active listings |
GET |
/api/listings/:ticketId |
Get listing by ticket ID |
DELETE |
/api/listings/:ticketId |
Cancel a listing |
GET |
/api/verify/:address/:ticketId |
Verify ticket ownership |
import { StellarTicketClient, Asset, Keypair } from "@stellar-ticket/sdk";
const client = new StellarTicketClient({
horizonUrl: "https://horizon-testnet.stellar.org",
network: "testnet",
});
// Build a strict-receive buy transaction
const tx = await client.buildBuyTicketTx({
buyerKeypair: Keypair.fromSecret("S..."),
sellerAddress: "G...",
ticketAsset: new Asset("TICKET001", "G...issuer"),
maxSendAmount: "15",
receiveAmount: "1",
});
await client.submitTx(tx);
// Verify ownership
const owns = await client.verifyOwnership({
address: "G...",
ticketAsset: new Asset("TICKET001", "G...issuer"),
});
// Pure utilities (no network call)
const { creator, seller } = StellarTicketClient.calculateRoyalty(10_000_000n, 500); // 5%
const valid = StellarTicketClient.validateResalePrice(12_000_000n, 10_000_000n, 15000); // 150% capcontracts/ 10 tests (cargo test)
backend/ 9 tests (jest)
frontend/ 4 tests (jest + @testing-library/react)
sdk/ 7 tests (jest)
Run everything:
# Contracts
(cd contracts && cargo test)
# Node packages
(cd backend && npm test)
(cd sdk && npm test)
(cd frontend && npm test)- Deploy contracts with
stellar contract deploy(requires stellar-cli and a funded account on testnet/mainnet) - Initialize the factory:
stellar contract invoke --id <FACTORY_ID> -- initialize --admin <ADMIN_ADDRESS> - Set
NEXT_PUBLIC_API_URLin the frontend and deploy (Vercel, etc.) - Run the backend behind a reverse proxy with TLS
- Dynamic pricing (bonding curve)
- Soulbound (non-transferable) tickets
- Ticket staking for VIP perks
- CAPTCHA + wallet signature pre-authorization
- Horizon event indexer for real-time ownership sync
- DAO-governed event parameters
MIT