Digital art marketplace where the royalty is enforced by the contract β not by the platform.
On every marketplace resale, the artist is paid before the sale can close. Or the sale doesn't happen.
π Live: molotov-web.vercel.app Β· βοΈ Stellar testnet Β· π¨ Built for artists in Latin America
π What is actually deployed (and what isn't): doc/status.md β read this before the roadmap.
When a digital artwork resells for more, the artist who made it usually sees nothing.
Web3 was supposed to fix this with royalties β a cut for the artist on every resale. In practice, that promise collapsed. To compete on lower fees, the major NFT marketplaces made royalties optional, dependent on the goodwill of the buyer or the platform. Creators were cut out again, this time with extra jargon on top.
The real problem isn't technical β it's about who captures the value. And that gets decided in exactly one place: the contract that moves the money.
Molotov puts the royalty where nobody can renegotiate it:
- The artist sets their royalty at mint, 1β15%, enforced by the contract β not by the form.
- Once minted it can never be changed. There are no royalty setters; the ERC-2981-style stubs panic
RoyaltiesImmutableAfterMint. - On every marketplace resale, the royalty is paid before the sale can close. If it can't be distributed, the whole sale reverts.
- The contract holds zero funds: it escrows the NFT and the buyer pays each recipient directly.
We call it inverted Spotify: income flows to the creator, not away from them.
The guarantee is: every marketplace resale distributes the royalty.
A plain SEP-50 transfer settled privately between two wallets bypasses the marketplace, and therefore the royalty. That is the standard NFT trade-off, and closing it would require transfer restrictions on the token itself β a different product decision, tracked on the mainnet roadmap. We would rather write this down than let someone discover it.
Within the marketplace, the guarantee has no exceptions. See doc/marketplace-invariants.md for the properties and the tests that lock each one in.
A 100 XLM resale with a 10% royalty:
| Recipient | Amount | Rule |
|---|---|---|
| Artist | 10 XLM | enforced by contract β cannot be skipped |
| Platform fee | 2.5 XLM | the only cut Molotov takes |
| Seller | 87.5 XLM | the remainder |
Every stroop is accounted for: treasury(fee) + Ξ£(royalty) + seller_remainder == price, exactly. Rounding dust from integer division lands deterministically on the last recipient, so nothing is created or lost. All arithmetic is checked, with overflow-checks = true in the release profile.
An optional referral share is carved out of the platform fee β never added to the price. The seller's cost is identical whether or not a sale was referred.
A "primary sale" is the one path that skips the royalty β and it exists for a good reason: on a first sale the royalty recipient is the seller, so paying it would just move money in a circle.
The problem is that if anyone could declare a sale "primary", the guarantee would be worthless: a reseller would simply list with primary_split = [100% to me] and pay the artist nothing.
So list gates it. When a primary_split is present, the contract reads nft.minter_of(token_id) and panics SplitNotAllowedForReseller unless the seller is the token's creator. A token whose minter was never recorded can never use a primary split at all β it must sell on the royalty-bearing path.
The result: every seller who is not the creator lands on the secondary path, where the royalty vector is paid verbatim before they receive their remainder.
Tests: b1_reseller_primary_split_rejected_at_list, b1_minter_primary_split_still_allowed, b1_reseller_without_split_pays_full_royalty, b1_legacy_token_without_minter_rejects_split.
Three Soroban contracts, each with one responsibility:
| Contract | Responsibility |
|---|---|
| MolotovNFT | The artwork token. Stores the immutable royalty and the minter, set at mint. |
| ArtistRegistry | Allowlist of artist addresses. See the note below on its current state. |
| Marketplace | Listings, NFT escrow, and the money distribution on every sale. |
β οΈ The artist gate is currently OFF on-chain. The NFT's registry pointer is set to a placeholder, so any wallet can mint during the open beta./adminoffers register/revoke, and revoking does hide an artist from/artists, but it does not currently control who can mint. Turning the gate on is aset_registrycall on the deployed NFT β a decision, not a build.
Off-chain indexer β the chain is the source of truth; we project its events into Supabase for fast reads. The projection is derived data, never authoritative:
Contracts (Stellar) ββ emit events
β
Soroban RPC (getEvents)
β
Indexer: fetch β decode β apply β advance cursor β»
β
Supabase (queryable projection, read-only to the client)
β
Web app (Next.js)
The browser is a trust boundary: every API route is read-only, no key ever touches the signing path, and the only writer to Supabase is the indexer. Row-level security gives the anon key SELECT and nothing else.
Deployed on Stellar testnet.
doc/status.mdis the single source for what is live right now β the network, the deployed contract IDs, and exactly which features are on or off.
The deployed contract IDs (with explorer links) and the active network live in doc/status.md β kept in one place rather than restated here, so they cannot drift out of sync.
- Mint β upload β IPFS (Pinata) β on-chain mint with the royalty written in.
- Sell and resell β fixed-price listings with NFT escrow; cancel returns the token.
- Buy β a single atomic invocation: payment, royalty, fee and delivery settle together or revert together.
- Artist earnings (
/earnings) β every primary sale and every resale royalty, read from what the indexer projects. The two are reported separately on purpose: the resale royalty is money that arrived after the work stopped being yours, and that is the whole argument. - Browse β
/works,/token/[id],/artists,/my-work, and an owner-gated/admin.
A cursor-driven poller (apps/web/app/api/indexer/) that projects mint, transfer, burn, list, sold, cancel and artist register/revoke events into Supabase.
- Idempotent β every
apply_*can be replayed safely; re-applying an event is a no-op. - Never skips β a failed apply aborts the poll before advancing the cursor, so an event cannot be silently dropped from the projection. The trade-off is deliberate: a genuinely bad event blocks the indexer until it is fixed, and
/api/indexer/healthsays exactly which one. - Ordering-safe β ownership only moves forward, stamped by
(ledger, event_index), so a partial replay of an old range cannot regresstokens.owner. - Health β
/api/indexer/healthreports cursor lag, margin to the RPC retention floor, and the last blocking error. It returns503when a threshold is breached, so an uptime monitor can watch it. - Authenticated β
/api/indexeris gated by a bearer secret and fails closed in production.
The contracts implement the Stellar ecosystem standards that let a third party integrate without bespoke work:
| Standard | Where |
|---|---|
| SEP-50 | MolotovNFT is a standard non-fungible token (transfer, burn, owner_of, balance, token_uri) via OpenZeppelin Stellar Contracts |
| SEP-49 | All three contracts are upgradeable in place, owner-gated |
| SEP-43 | Wallet connection through Stellar Wallets Kit β any conforming wallet works |
Claims here are checkable by cloning the repo and running the commands.
-
Contract tests β
cd contracts && cargo test --workspace. Property-based tests over the distribution math (conservation, dust, non-negativity, clean overflow), boundary cases, TTL lifetime tests, and XDR-level event assertions. -
Mutation testing (cargo-mutants) β a full run over all three contracts generates 140 mutants; the suite currently catches 125, with 15 unviable and 0 surviving. Reproduce with
cd contracts && cargo mutants.The four that survived the first full run were real gaps, all in the NFT: nothing asserted that a royalty of exactly 1% or exactly 15% is accepted (only that outside the range is rejected), that
get_royalty_infotolerates a zero sale price, or thatburn_fromdoes anything at all. Tests were added rather than the claim narrowed. -
Static analysis β CoinFabrik Scout runs on every push touching
contracts/, via.github/workflows/scout-audit.yml. -
CI β contract tests, typecheck, lint and web tests run on every push to
mainand every pull request (.github/workflows/ci.yml). -
Indexer decoding β tested against a committed fixture of real testnet XDR, not synthetic events.
-
Conservation verified live β a 100 XLM secondary sale with a 10% royalty and 2.5% fee settles to the stroop, zero residual.
@molotov/indexer-db-tests (25 tests) does not run in CI, and nobody runs it automatically.
It is an integration suite that exercises the database layer directly β the RLS policies, the SECURITY DEFINER writer functions, and the idempotency of apply_* β against a local Supabase, which needs Docker. In CI there is no such instance, so every test fails on a connection error. Rather than let that make the pipeline permanently red (a red pipeline is a pipeline nobody reads), it is excluded.
The consequence is worth stating plainly: the tests that cover the database's security model are the ones with no automation behind them. Run them by hand after touching anything under supabase/:
supabase start # needs Docker
pnpm --filter=@molotov/indexer-db-tests testWiring this into CI means booting the local stack in the job β worth doing, not done yet.
Why Stellar. Low fees and fast settlement make micro-royalties economically viable. On high-gas chains, network fees can swallow a small royalty whole.
How Molotov makes money. A 2.5% platform fee on every sale, set at contract construction and currently live on testnet. For comparison, objkt β the leading Tezos art marketplace β charges 5%. Revenue scales with volume, and the fee is the only cut the platform takes: the royalty is paid by the buyer, not carved out of Molotov's share.
The moat. The royalty guarantee lives in the contract: public, immutable, verifiable. Artists don't have to trust the platform. The code is the policy.
Who it's for. Contemporary digital artists in Latin America who want their work to earn over time, not only at first sale. Editorial and gallery-first, deliberately anti-crypto-bro.
| Layer | Stack |
|---|---|
| Web app | Next.js 16 Β· React 19 Β· Tailwind CSS 4 |
| Smart contracts | Soroban (Rust) Β· OpenZeppelin Stellar Contracts |
| Contract bindings | Generated with stellar contract bindings typescript β never hand-edited |
| Wallets | Stellar Wallets Kit Β· Freighter Β· xBull Β· Albedo Β· LOBSTR Β· Hana |
| Auth / embedded wallet | Privy (email + Google) β testnet only, see below |
| Indexer + off-chain data | Supabase (PostgreSQL, RLS, SECURITY DEFINER writers) |
| File storage | IPFS via Pinata |
| Monorepo | pnpm workspaces + Turborepo |
Privy is gated to testnet. The email path derives a Stellar keypair and keeps the secret in
localStorage, which is fine for a demo and unacceptable for real funds. It is disabled outside testnet and will be replaced by smart accounts with passkeys before mainnet.
Requirements: Node 20, pnpm 10, Rust with the wasm32v1-none target, Stellar CLI.
git clone https://github.com/BuenDia-Builders/molotov.git
cd molotov
pnpm install
cp apps/web/.env.example apps/web/.env.local # Supabase, Pinata and Privy keys
pnpm dev| Command | What it does |
|---|---|
pnpm dev |
Run the web app in development |
pnpm build |
Build the workspace |
pnpm lint |
ESLint |
pnpm typecheck |
tsc --noEmit across the workspace |
pnpm --filter=web test |
Web + indexer unit tests |
cd contracts && cargo test --workspace |
The contract test suite |
pnpm testat the root also runs@molotov/indexer-db-tests, an integration suite that needs a local Supabase (supabase start, requires Docker). Without the local stack those tests fail on a connection error β use the scoped commands above instead.
apps/web Next.js app + the indexer API routes
contracts Soroban contracts: nft, artist-registry, marketplace
packages/stellar-client Generated TypeScript bindings
supabase Migrations and integration tests
doc Architecture, contracts, flows, indexer spec, invariants
Nothing in this section exists yet.
Toward mainnet
- Move the upgrade key to a multisig with a timelock. Today all three contracts share a single owner key, which means "immutable" is only as strong as that key.
- Replace the Privy email path with smart accounts + passkeys, so onboarding does not require a browser extension and no secret ever lives in
localStorage. - Centralize network configuration; today the testnet/mainnet switch is not a single source of truth.
- Add an emergency pause (
Pausable) to the marketplace. (The allowlist of NFT contracts the marketplace will settle is already implemented in the contract βset_allowed_nft, checked inlist/buyβ and pending deployment; seedoc/status.md.) - Decide and document the royalty-recipient trustline requirement.
Product
- USDC-denominated listings so artists price in dollars and Stellar stays invisible.
- WalletConnect, making responsive web the mobile story for this stage β there is no native app, and
apps/mobile/is an empty placeholder. - Bank withdrawal for Argentine artists (ARS or USD).
- Portuguese. Spanish and English both ship today; Spanish is the product's voice.
Molotov β digital art where creating earns you a permanent stake in what you made.
Built for PULSO Hackathon 2026 β NearX Γ Stellar Development Foundation