build.thiri.ai
Solana · Seeker · Cadenza
Anchor Program v1

Cadenza — Technical Specification

On-chain royalty, licensing, and provenance rail. 5 account schemas, 14 instructions, deployed as a Solana Anchor program.

Source: docs/CADENZA_SPEC.md

Cadenza — Technical Specification v1

Status: Draft Author: Dennison / Blues Prince Media Date: 2026-04-19 Program: Solana Anchor (Rust) Network: Devnet → Mainnet (Week 0, May 5)


1. What Cadenza Is

Cadenza is the on-chain royalty, licensing, and provenance rail that powers the commerce half of the THIRI × Cadenza ecosystem. It’s a single Solana program (deployed via Anchor) that provides three core operations:

  1. Mint — Create a music asset with cryptographic provenance
  2. Split — Automate royalty distribution between collaborators
  3. License — Grant on-chain usage rights with enforceable terms

Every app in the ecosystem either writes to or reads from Cadenza:

WRITES TO CADENZA                    READS FROM CADENZA
─────────────────                    ──────────────────
Mint Studio      → CadenzaRecord     Royalty Console   ← CadenzaRecord
Collab Split     → SplitConfig       Fan Market        ← LicenseGrant
Player THIRI     → LicenseGrant      Skill Logger      ← all accounts
Skill Logger     → FingerprintRef    Key Finder        ← (future) provenance

What Cadenza is NOT:

  • Not a token. No SPL token launch, no AMM, no liquidity pool.
  • Not a marketplace contract. Fan Market is a separate frontend; Cadenza is the settlement layer beneath it.
  • Not a competitor to Metaplex. Cadenza uses Metaplex Core for the NFT standard and adds a music-specific metadata layer on top.

2. Account Architecture

2.1 Program Accounts

All accounts are PDAs (Program Derived Addresses) owned by the Cadenza program.

┌─────────────────────────────────────────────────────────┐
│                    CADENZA PROGRAM                       │
│                                                         │
│  ┌───────────────┐    ┌───────────────┐                │
│  │ PublisherNFT   │    │ CadenzaRecord │                │
│  │ (1 per dev)    │───▶│ (1 per mint)  │                │
│  └───────────────┘    └───────┬───────┘                │
│                               │                         │
│                    ┌──────────┼──────────┐              │
│                    ▼          ▼          ▼              │
│             ┌──────────┐ ┌────────┐ ┌────────────────┐ │
│             │SplitConfig│ │License │ │FingerprintRef  │ │
│             │(per collab)│ │Grant   │ │(per user)      │ │
│             └──────────┘ └────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────┘

2.2 Account Schemas (Anchor IDL)

PublisherNFT

The cryptographic developer identity. Minted once during the Mainnet Foundation Sprint. Every subsequent app and mint links back to this.

#[account]
pub struct PublisherNft {
    /// The wallet that owns this publisher identity
    pub authority: Pubkey,            // 32 bytes
    /// Human-readable publisher name
    pub name: String,                 // 4 + 64 bytes max
    /// URI to off-chain metadata (logo, bio, links)
    pub metadata_uri: String,         // 4 + 200 bytes max
    /// Number of CadenzaRecords minted under this publisher
    pub mint_count: u64,              // 8 bytes
    /// Timestamp of creation
    pub created_at: i64,              // 8 bytes
    /// Bump seed for PDA derivation
    pub bump: u8,                     // 1 byte
}
// PDA seeds: ["publisher", authority.key()]
// Total: ~317 bytes + discriminator

CadenzaRecord

Attached to every music NFT minted through Mint Studio. This is the music-specific metadata that Metaplex Core doesn’t provide.

#[account]
pub struct CadenzaRecord {
    /// The publisher who minted this
    pub publisher: Pubkey,            // 32 bytes
    /// The Metaplex Core asset address
    pub asset_mint: Pubkey,           // 32 bytes
    /// Original creator wallet(s) — up to 5
    pub creators: Vec<Pubkey>,        // 4 + (32 × 5) = 164 bytes max
    /// Music-specific metadata
    pub title: String,                // 4 + 100 bytes max
    pub key_signature: Option<String>,// 1 + 4 + 12 bytes (e.g. "Bb minor")
    pub tempo_bpm: Option<u16>,       // 1 + 2 bytes
    pub duration_secs: Option<u32>,   // 1 + 4 bytes
    pub genre: Option<String>,        // 1 + 4 + 32 bytes max
    /// WoodShed fingerprint hash (SHA-256 of harmonic analysis)
    pub fingerprint_hash: Option<[u8; 32]>, // 1 + 32 bytes
    /// Off-chain audio URI (Arweave / IPFS)
    pub audio_uri: String,            // 4 + 200 bytes max
    /// Off-chain artwork URI
    pub artwork_uri: String,          // 4 + 200 bytes max
    /// Licensing terms
    pub license_type: LicenseType,    // 1 byte (enum)
    /// Whether secondary sales are allowed
    pub secondary_allowed: bool,      // 1 byte
    /// Royalty basis points for secondary sales (e.g. 500 = 5%)
    pub royalty_bps: u16,             // 2 bytes
    /// Active split config (if collaborators exist)
    pub split_config: Option<Pubkey>, // 1 + 32 bytes
    /// Timestamps
    pub created_at: i64,              // 8 bytes
    pub updated_at: i64,              // 8 bytes
    /// Bump
    pub bump: u8,                     // 1 byte
}
// PDA seeds: ["cadenza_record", asset_mint.key()]
// Total: ~893 bytes max + discriminator

SplitConfig

Defines how royalties are distributed between collaborators. Created by Collab Split, referenced by Cadenza Record.

#[account]
pub struct SplitConfig {
    /// The CadenzaRecord this split applies to
    pub cadenza_record: Pubkey,       // 32 bytes
    /// Who can modify this split (typically the primary creator)
    pub authority: Pubkey,            // 32 bytes
    /// Splits — up to 10 recipients
    pub splits: Vec<Split>,          // 4 + (40 × 10) = 404 bytes max
    /// Whether the split is locked (immutable after lock)
    pub locked: bool,                 // 1 byte
    /// Timestamps
    pub created_at: i64,              // 8 bytes
    pub locked_at: Option<i64>,       // 1 + 8 bytes
    /// Bump
    pub bump: u8,                     // 1 byte
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct Split {
    /// Recipient wallet
    pub recipient: Pubkey,            // 32 bytes
    /// Basis points (e.g. 5000 = 50%)
    pub share_bps: u16,              // 2 bytes
    /// Role description (e.g. "producer", "vocalist")
    pub role: String,                 // 4 + 2 bytes (index into role enum)
}
// PDA seeds: ["split", cadenza_record.key()]

LicenseGrant

On-chain record that wallet X has a license to use asset Y under specific terms. Created when a license is purchased through Fan Market or Player THIRI.

#[account]
pub struct LicenseGrant {
    /// The CadenzaRecord being licensed
    pub cadenza_record: Pubkey,       // 32 bytes
    /// The wallet receiving the license
    pub licensee: Pubkey,             // 32 bytes
    /// The wallet that granted the license
    pub licensor: Pubkey,             // 32 bytes
    /// License terms
    pub license_type: LicenseType,    // 1 byte
    /// Usage scope
    pub usage: UsageScope,            // 1 byte (enum)
    /// Duration (None = perpetual)
    pub expires_at: Option<i64>,      // 1 + 8 bytes
    /// Price paid in lamports
    pub price_lamports: u64,          // 8 bytes
    /// Whether license is active
    pub active: bool,                 // 1 byte
    /// Timestamps
    pub created_at: i64,              // 8 bytes
    pub revoked_at: Option<i64>,      // 1 + 8 bytes
    /// Bump
    pub bump: u8,                     // 1 byte
}
// PDA seeds: ["license", cadenza_record.key(), licensee.key()]

FingerprintRef

Links a user’s WoodShed fingerprint to their on-chain identity. Written by Skill Logger.

#[account]
pub struct FingerprintRef {
    /// The wallet this fingerprint belongs to
    pub owner: Pubkey,                // 32 bytes
    /// SHA-256 hash of the latest WoodShed fingerprint
    pub fingerprint_hash: [u8; 32],   // 32 bytes
    /// Number of analysis sessions that contributed
    pub session_count: u64,           // 8 bytes
    /// Off-chain URI to the full fingerprint data
    pub data_uri: String,             // 4 + 200 bytes max
    /// Timestamps
    pub created_at: i64,              // 8 bytes
    pub updated_at: i64,              // 8 bytes
    /// Bump
    pub bump: u8,                     // 1 byte
}
// PDA seeds: ["fingerprint", owner.key()]

2.3 Enums

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum LicenseType {
    /// All rights reserved — no licensing without explicit grant
    AllRightsReserved,
    /// Creative Commons Attribution
    CcBy,
    /// Creative Commons Attribution-ShareAlike
    CcBySa,
    /// Creative Commons Attribution-NonCommercial
    CcByNc,
    /// Sync license — for video/film/content use
    Sync,
    /// Sample license — can be chopped/remixed
    Sample,
    /// Custom — terms defined off-chain
    Custom,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum UsageScope {
    /// Personal / non-commercial use
    Personal,
    /// Commercial use (streaming, content creation)
    Commercial,
    /// Derivative works (remixes, samples)
    Derivative,
    /// Synchronization (video, film, ads)
    Synchronization,
    /// Unlimited — all usage rights
    Unlimited,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum CreatorRole {
    Producer,
    Vocalist,
    Songwriter,
    Instrumentalist,
    Engineer,
    Arranger,
    Lyricist,
    Other,
}

3. Instructions (Program API)

3.1 Publisher Operations

InstructionSignerDescription
initialize_publisherauthorityCreate PublisherNFT PDA; one-time setup
update_publisherauthorityUpdate name, metadata_uri

3.2 Mint Operations (Mint Studio)

InstructionSignerDescription
mint_cadenza_recordpublisher.authorityCreate Metaplex Core asset + CadenzaRecord PDA. Requires PublisherNFT.
update_cadenza_recordpublisher.authorityUpdate mutable fields (metadata, URIs, license type)
burn_cadenza_recordpublisher.authorityBurn the asset and close the CadenzaRecord account

mint_cadenza_record flow:

1. Verify signer holds PublisherNFT
2. Create Metaplex Core asset (CPI to mpl_core)
3. Initialize CadenzaRecord PDA with music metadata
4. Increment publisher.mint_count
5. Emit MintEvent { asset_mint, publisher, title, timestamp }

3.3 Split Operations (Collab Split)

InstructionSignerDescription
create_splitcadenza_record.publisher.authorityCreate SplitConfig for a CadenzaRecord. Splits must sum to 10000 bps.
update_splitsplit.authorityModify split percentages (only while unlocked)
lock_splitsplit.authorityPermanently freeze the split config
distribute_splitany (permissionless crank)Disburse SOL held in escrow according to split percentages

distribute_split flow:

1. Read SplitConfig.splits[]
2. Calculate each recipient's share of escrow balance
3. Transfer SOL to each recipient wallet
4. Emit DistributeEvent { cadenza_record, amounts[], recipients[], timestamp }

3.4 License Operations (Fan Market / Player THIRI)

InstructionSignerDescription
grant_licenselicensor (asset owner)Create LicenseGrant PDA; transfer payment to licensor (or escrow → split)
revoke_licenselicensorDeactivate a LicenseGrant
verify_licenseany (read-only)Check if a wallet holds an active license for an asset

grant_license flow:

1. Verify licensor owns the CadenzaRecord's asset
2. Transfer price_lamports from licensee → escrow
3. If SplitConfig exists: distribute via split percentages
4. Else: transfer full amount to licensor
5. Create LicenseGrant PDA
6. Emit LicenseEvent { cadenza_record, licensee, license_type, price, timestamp }

3.5 Fingerprint Operations (Skill Logger)

InstructionSignerDescription
register_fingerprintownerCreate or update FingerprintRef PDA
increment_sessionsownerBump session_count; called after each THIRI app interaction

4. How Each App Connects to Cadenza

4.1 App → Instruction Mapping

AppInstructions UsedData Flow
Mainnet Foundationinitialize_publisherCreates the PublisherNFT; one-time bootstrap
Mint Studiomint_cadenza_record, update_cadenza_recordArtist creates music NFTs; all metadata written to CadenzaRecord
Royalty Console— (read-only)Reads CadenzaRecords + SplitConfigs to display royalty dashboard
Collab Splitcreate_split, update_split, lock_split, distribute_splitManages multi-wallet royalty splits
Stem Splitter— (indirect)Stems could be minted as derivative CadenzaRecords (future)
Key Finder— (indirect)Detected key feeds CadenzaRecord.key_signature at mint time
Chord Analyzer— (indirect)Chord data feeds CadenzaRecord.fingerprint_hash
Ear Trainerincrement_sessionsEach drill session bumps the user’s FingerprintRef
Player THIRIgrant_license, verify_licensePlayback checks license; provenance mint on remix
Key Transposer— (read-only)Reads CadenzaRecord.key_signature for context
Setlist Planner— (read-only)Reads tempo, key, duration from CadenzaRecords
Fan Marketgrant_license, revoke_licenseTwo-sided marketplace; license purchase triggers split distribution
Skill Loggerregister_fingerprint, increment_sessionsAggregates cross-app usage into on-chain identity

4.2 WoodShed ↔ Cadenza Bridge

The WoodShed engine exports types that map directly to Cadenza on-chain fields:

WoodShed TypeCadenza FieldTransformation
ChordData.symbolCadenzaRecord.key_signatureKey Finder resolves root + quality → key string
MIDIData.tempoCadenzaRecord.tempo_bpmDirect mapping (u16)
Voicing.midiNotes[]FingerprintRef.fingerprint_hashSHA-256 of accumulated voicing choices
StyleConfig.nameCadenzaRecord.genreStyle name → genre string
ReharmonizedProgressionCadenzaRecord.fingerprint_hashReharmonization patterns feed the fingerprint

5. Security Model

5.1 Access Control

AccountWho Can WriteWho Can Read
PublisherNFTauthority onlyanyone
CadenzaRecordpublisher.authority onlyanyone
SplitConfigsplit.authority (until locked)anyone
LicenseGrantlicensor (create/revoke)anyone (verify)
FingerprintRefowner onlyanyone

5.2 Invariants (enforced by the program)

  1. Split percentages must sum to exactly 10,000 bps — any other value is rejected
  2. Locked splits are immutable — once lock_split is called, no further updates
  3. One PublisherNFT per wallet — PDA derivation enforces uniqueness
  4. One FingerprintRef per wallet — same mechanism
  5. License grants are unique per (asset, licensee) pair — PDA seeds enforce this
  6. Only asset owners can license — verified via Metaplex Core ownership check
  7. Payments are atomic — SOL transfer + PDA creation in one transaction

5.3 Rent Exemption

All accounts are rent-exempt (Solana standard since 1.15). Account sizes:

AccountMax SizeRent (SOL)
PublisherNFT~325 bytes~0.0032
CadenzaRecord~900 bytes~0.0082
SplitConfig~490 bytes~0.0046
LicenseGrant~145 bytes~0.0018
FingerprintRef~295 bytes~0.0030

6. Events

All instructions emit CPI events for indexing by off-chain services (Helius, SimpleHash, custom indexer).

#[event]
pub struct MintEvent {
    pub asset_mint: Pubkey,
    pub publisher: Pubkey,
    pub title: String,
    pub timestamp: i64,
}

#[event]
pub struct DistributeEvent {
    pub cadenza_record: Pubkey,
    pub total_lamports: u64,
    pub recipients: Vec<Pubkey>,
    pub amounts: Vec<u64>,
    pub timestamp: i64,
}

#[event]
pub struct LicenseEvent {
    pub cadenza_record: Pubkey,
    pub licensee: Pubkey,
    pub licensor: Pubkey,
    pub license_type: LicenseType,
    pub price_lamports: u64,
    pub timestamp: i64,
}

#[event]
pub struct FingerprintEvent {
    pub owner: Pubkey,
    pub fingerprint_hash: [u8; 32],
    pub session_count: u64,
    pub timestamp: i64,
}

7. Client SDK (TypeScript)

The Cadenza TypeScript SDK wraps the Anchor IDL for frontend consumption. Ships as @bluesprince/cadenza-sdk alongside @bluesprince/woodshed-engine.

// @bluesprince/cadenza-sdk — public API surface

// ── Connection ──────────────────────────────────────────────
export function createCadenzaClient(
  connection: Connection,
  wallet: AnchorWallet,
): CadenzaClient;

// ── Publisher ───────────────────────────────────────────────
export async function initializePublisher(
  client: CadenzaClient,
  name: string,
  metadataUri: string,
): Promise<{ publisherPda: PublicKey; tx: string }>;

// ── Minting ─────────────────────────────────────────────────
export async function mintCadenzaRecord(
  client: CadenzaClient,
  params: {
    title: string;
    audioUri: string;
    artworkUri: string;
    keySignature?: string;
    tempoBpm?: number;
    durationSecs?: number;
    genre?: string;
    fingerprintHash?: Uint8Array;
    licenseType: LicenseType;
    royaltyBps: number;
  },
): Promise<{ assetMint: PublicKey; recordPda: PublicKey; tx: string }>;

// ── Splits ──────────────────────────────────────────────────
export async function createSplit(
  client: CadenzaClient,
  cadenzaRecord: PublicKey,
  splits: Array<{ recipient: PublicKey; shareBps: number; role: CreatorRole }>,
): Promise<{ splitPda: PublicKey; tx: string }>;

export async function distributeSplit(
  client: CadenzaClient,
  splitConfig: PublicKey,
): Promise<{ tx: string; amounts: Record<string, number> }>;

export async function lockSplit(
  client: CadenzaClient,
  splitConfig: PublicKey,
): Promise<{ tx: string }>;

// ── Licensing ───────────────────────────────────────────────
export async function grantLicense(
  client: CadenzaClient,
  params: {
    cadenzaRecord: PublicKey;
    licensee: PublicKey;
    licenseType: LicenseType;
    usage: UsageScope;
    priceLamports: number;
    expiresAt?: number;
  },
): Promise<{ licensePda: PublicKey; tx: string }>;

export async function verifyLicense(
  client: CadenzaClient,
  cadenzaRecord: PublicKey,
  licensee: PublicKey,
): Promise<{ active: boolean; grant: LicenseGrant | null }>;

// ── Fingerprint ─────────────────────────────────────────────
export async function registerFingerprint(
  client: CadenzaClient,
  fingerprintHash: Uint8Array,
  dataUri: string,
): Promise<{ fingerprintPda: PublicKey; tx: string }>;

// ── Queries ─────────────────────────────────────────────────
export async function getPublisher(
  client: CadenzaClient,
  authority: PublicKey,
): Promise<PublisherNft | null>;

export async function getRecordsByPublisher(
  client: CadenzaClient,
  publisher: PublicKey,
): Promise<CadenzaRecord[]>;

export async function getLicensesByWallet(
  client: CadenzaClient,
  wallet: PublicKey,
): Promise<LicenseGrant[]>;

8. Fee Structure

OperationFeeRecipient
initialize_publisherFree (rent only)
mint_cadenza_record0.01 SOLBPM treasury
create_splitFree (rent only)
distribute_split1% of disbursementBPM treasury
grant_license2.5% of priceBPM treasury
register_fingerprintFree (rent only)

Treasury wallet: BPM... (to be generated during Mainnet Foundation Sprint)


9. Deployment Plan

Phase 0: Devnet (Week -1, Apr 28 – May 4)

# Install toolchain
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
cargo install --avm anchor-cli

# Initialize project
cd commerce/cadenza
anchor init cadenza-program --javascript

# Deploy to devnet
anchor build
anchor deploy --provider.cluster devnet

Phase 1: Mainnet (Week 0, May 5)

# Switch to mainnet
anchor deploy --provider.cluster mainnet-beta

# Initialize PublisherNFT
npx ts-node scripts/init-publisher.ts \
  --name "Blues Prince Media" \
  --metadata-uri "https://thiri.ai/publisher.json"

Phase 2: Client SDK (Week 0-1)

# Generate TypeScript types from IDL
anchor build  # generates target/idl/cadenza.json
npx @coral-xyz/anchor gen-ts --idl target/idl/cadenza.json

# Publish SDK
cd sdk
npm publish --access public

10. Testing Strategy

Unit Tests (Anchor)

tests/
├── publisher.test.ts    — init, update, uniqueness constraint
├── mint.test.ts         — mint, update, burn, publisher verification
├── split.test.ts        — create, update, lock, invariant (sum = 10000)
├── license.test.ts      — grant, revoke, verify, payment flow
├── fingerprint.test.ts  — register, increment, uniqueness
└── integration.test.ts  — full flow: publish → mint → split → license → distribute

Key Test Cases

  1. Split invariant: reject splits that don’t sum to 10,000 bps
  2. Lock immutability: reject updates after lock_split
  3. License uniqueness: reject duplicate license for same (asset, wallet)
  4. Payment atomicity: verify SOL transfers match split percentages
  5. Publisher uniqueness: reject second PublisherNFT for same wallet
  6. Ownership verification: reject license grants from non-owners
  7. Rent exemption: verify all accounts satisfy minimum rent

11. Open Questions

[!IMPORTANT] These need decisions before Week 0 deploy.

  1. Metaplex Core vs. Token-2022? — Core is newer, simpler, cheaper. Token-2022 has wider wallet support. Recommendation: Core (Seeker wallets support it; simpler account model).

  2. Arweave vs. IPFS for audio storage? — Arweave is permanent but costs ~$0.10/MB. IPFS is free but needs pinning. Recommendation: Arweave for masters, IPFS for previews.

  3. Should SplitConfig support time-based splits? (e.g., Producer gets 80% first 90 days, then 50% forever). Not in v1 — adds complexity. Can add SplitSchedule account later.

  4. Treasury multisig or single wallet? — Multisig (Squads Protocol) is safer but adds deployment complexity. Recommendation: Start single, migrate to Squads at >$10K monthly volume.

  5. Should Key Finder / Chord Analyzer auto-populate CadenzaRecord fields? — Yes, but only at mint time (not retroactively). WoodShed analysis results are passed as instruction args to mint_cadenza_record.


12. Relationship to Existing THIRI API

The existing commerce/thiri-api handles off-chain license management (Stripe checkout, Supabase license keys) for the THIRI VST plugins. Cadenza handles on-chain licensing for the Seeker dApp ecosystem.

They coexist:

THIRI APICadenza
What it licensesVST plugins (Voice, Keys, Seq)Music assets (recordings, stems, compositions)
Payment railStripe (fiat)Solana (SOL)
StorageSupabaseSolana accounts
TargetDesktop producersMobile Seeker users
VerificationAPI call to /api/verifyOn-chain PDA lookup

Future bridge: When a THIRI Pro user mints a track produced in the VST, the THIRI API verifies their license → Cadenza mints the CadenzaRecord. This connects the desktop production workflow to the mobile distribution workflow.


Last updated: 2026-04-19 · Blues Prince Media