Skip to main content

Marketplace Program

The CollectorCrypt Marketplace Program is a Solana smart contract that facilitates decentralized trading of NFTs and digital collectibles using USDC.

Program Details

  • Program ID: CcmRKTuZCGJBWQwMHvDYApBRvSZNHqGJXkznqpDTSQUr
  • Framework: Anchor (Solana)
  • Blockchain: Solana
  • Currency: USDC (SPL Token)
  • Version: 5

Devnet USDC Faucet

Use this faucet to get the USDC token used on devnet: https://spl-token-faucet.com/?token-name=USDC-Dev

Overview

The marketplace supports two types of NFTs:

  • Programmable NFTs (pNFTs): Token Metadata standard with enforced royalties and transfer delegates
  • Compressed NFTs (cNFTs): Merkle-tree-based assets via Metaplex Bubblegum

Key Features

  • Non-custodial: NFTs remain in seller wallets using delegation, not escrow transfers
  • Network-agnostic: Environment values injected via initialization
  • Deterministic: All PDAs use fixed, reproducible seed schemes
  • Whitelisting: Collection-based access control
  • Platform fees: Configurable basis points (0.1% - 10%)
  • User escrow: USDC escrow system for offer-backed liquidity
Escrowless System - User Responsibility

Because this is an escrowless marketplace, NFTs remain in your wallet while listed. You are responsible for canceling listings on NFTs you transfer away. While our backend monitors transfers and attempts to auto-cancel listings, this process is not 100% reliable. If you transfer an NFT and later receive it back, the old listing may still be active. Always verify and cancel stale listings manually.

Architecture

Transaction Flows

Listing Flow

  1. Seller initiates list_pnft or list_cnft
  2. Creates Listing PDA
  3. Delegates NFT to the Listing PDA

Purchase Flow (Direct Buy)

  1. Buyer initiates buy_pnft or buy_cnft
  2. Validates whitelist
  3. Transfers USDC (price) to seller
  4. Transfers USDC (fee) to treasury
  5. Transfers NFT from seller to buyer
  6. Closes Listing PDA

Offer Flow

  1. Buyer deposits funds via deposit_to_user_escrow
  2. Buyer creates offer via make_offer or make_offer_n_deposit
  3. Creates Offer PDA
  4. Validates whitelist

Accept Offer Flow

  1. Seller initiates accept_offer_for_pnft or accept_offer_for_cnft
  2. Transfers USDC from escrow (price - fee) to seller
  3. Transfers USDC from escrow (fee) to treasury
  4. Transfers NFT from seller to buyer
  5. Closes Offer PDA

Account Structure

Market (Singleton)

PDA Seeds: ["market"]
Size: 186 bytes

FieldTypeDescription
versionu32Protocol version (currently 5)
super_adminPubkeyGovernance authority
market_adminPubkeyOperational authority
backend_accountPubkeyBackend co-signer for embedded wallets
market_bump[u8; 1]PDA bump seed
treasuryPubkeyFee collection address
royalty_feeu16Platform fee in basis points
min_offer_amountu64Minimum offer amount (USDC lamports)
pausedboolGlobal pause flag
cnft_depthu8Compressed NFT Merkle tree depth
cnft_canopyu8Compressed NFT canopy depth
usdc_mintPubkeyValidated USDC mint address

Listing

PDA Seeds: ["listing", asset_id, seller]
Size: 130 bytes

FieldTypeDescription
sellerPubkeyWallet that listed the NFT
nftPubkeyNFT mint (pNFT) or asset ID (cNFT)
listing_bump[u8; 1]PDA bump seed
priceu64Listing price in USDC lamports
collection_typeCollectionTypePNFT or CNFT
created_ati64Unix timestamp of creation
updated_ati64Unix timestamp of last update
rent_payerPubkeyWho paid rent (for refund on close)

Offer

PDA Seeds: ["offer", asset_id, buyer]
Size: 129 bytes

FieldTypeDescription
nftPubkeyNFT being offered on
buyerPubkeyWallet making the offer
offer_bump[u8; 1]PDA bump seed
priceu64Offer amount in USDC lamports
created_ati64Unix timestamp of creation
updated_ati64Unix timestamp of last update
rent_payerPubkeyWho paid rent (for refund on close)

UserEscrow

PDA Seeds: ["user_escrow", user]
Size: 49 bytes

FieldTypeDescription
userPubkeyEscrow owner
user_escrow_bump[u8; 1]PDA bump seed
balanceu64Tracked USDC balance

CollectionWhitelistEntry (V2)

PDA Seeds: ["whitelist_entry", market, collection_type_str, collection]
Size: 50 bytes

FieldTypeDescription
collectionPubkeyWhitelisted collection address
collection_typeCollectionTypePNFT or CNFT
added_ati64Timestamp when whitelisted
bumpu8PDA bump seed

Role & Authority Model

Role Hierarchy

RoleHolderCapabilities
SUPER ADMINSquads multisig or governance walletTransfer super admin role, set market admin, set platform fee (10-1000 bps), set USDC mint, set backend account, set treasury

| BACKEND ACCOUNT | Backend service | Co-signs transactions for embedded wallets, pays rent for PDA creation (off-chain only) | | TREASURY | Fee recipient | Receives platform fees on every trade |

Program Instructions

Initialization

initialize

Purpose: Create the singleton Market account with initial configuration

Signers: payer (any wallet)

Parameters:

  • super_admin: Pubkey
  • market_admin: Pubkey
  • backend_account: Pubkey
  • treasury: Pubkey

Accounts:

  • payer (mut, signer)
  • market (init PDA)
  • usdc_mint (validated Mint)
  • system_program

Notes:

  • One-time only (second call fails)
  • Permissionless but singleton PDA ensures only one Market exists
  • USDC mint validated as existing SPL Mint account

Listing Instructions

list_pnft

Purpose: Create a listing for a programmable NFT

Signers: seller, rent_payer

Parameters: price: u64 (USDC lamports)

Key Accounts:

  • seller
  • rent_payer
  • market
  • whitelist_entry
  • asset_id (Mint)
  • listing (init PDA)
  • seller_nft_account
  • Metadata/edition/token_record accounts
  • Authorization rules accounts

Preconditions:

  • Marketplace not paused
  • Collection whitelisted
  • Listing does not already exist
  • price > 0

State: Creates Listing PDA. Delegates NFT transfer authority to the Listing PDA.

Important: Because this is an escrowless system, the NFT remains in your wallet. If you transfer the NFT to another wallet, you must cancel the listing. The backend attempts to auto-cancel listings on transfers, but this is not guaranteed.

list_cnft

Purpose: Create a listing for a compressed NFT

Signers: seller, rent_payer

Parameters:

  • price: u64
  • root: [u8; 32]
  • data_hash: [u8; 32]
  • creator_hash: [u8; 32]
  • asset_data_hash: [u8; 32]
  • collection_hash: [u8; 32]
  • nonce: u64
  • index: u32

Key Accounts:

  • seller, rent_payer, market, whitelist_entry
  • asset_id, listing (init PDA)
  • merkle_tree, tree_config
  • Bubblegum/compression programs

State: Creates Listing PDA. Delegates cNFT to Listing PDA. Merkle proof accounts passed via remaining_accounts.

Important: Because this is an escrowless system, the cNFT remains in your wallet. If you transfer the cNFT to another wallet, you must cancel the listing. The backend attempts to auto-cancel listings on transfers, but this is not guaranteed.

update_listing

Purpose: Update the price of an existing listing

Signers: seller

Parameters: new_price: u64

Preconditions:

  • Marketplace not paused
  • new_price > 0
  • Listing exists
  • Seller matches listing.seller

State: Updates listing.price and listing.updated_at

cancel_pnft_listing

Purpose: Cancel a pNFT listing and revoke the delegate

Signers: seller

Key Accounts:

  • seller
  • rent_receiver (validated against listing.rent_payer)
  • listing (closed)
  • NFT accounts, metadata accounts

State: Revokes TransferV1 delegate from Listing PDA. Closes Listing PDA; rent refunded.

cancel_cnft_listing

Purpose: Cancel a cNFT listing and optionally revoke the delegate

Signers: seller

Parameters: Merkle proof parameters + skip_delegate_revoke: bool

State: If !skip_delegate_revoke, re-delegates cNFT from Listing PDA back to seller. Closes Listing PDA.


Trade Instructions

buy_pnft

Purpose: Purchase a listed pNFT directly

Signers: buyer, rent_payer

Parameters: expected_price: u64

Key Accounts:

  • buyer, seller, listing_rent_receiver, rent_payer
  • market, whitelist_entry, asset_id, listing
  • seller/buyer NFT accounts, usdc_mint
  • buyer/seller USDC accounts, treasury
  • metadata/edition/token record accounts

Preconditions:

  • Marketplace not paused
  • listing.price == expected_price (frontrun protection)
  • Collection still whitelisted at trade time

State Transitions:

  1. USDC price transferred from buyer to seller
  2. USDC fee transferred from buyer to treasury
  3. pNFT transferred from seller to buyer (via Listing PDA)
  4. Listing PDA closed; rent refunded

Important: Buyer pays price + fee. Seller receives full price.

buy_cnft

Purpose: Purchase a listed cNFT directly

Signers: buyer, rent_payer

Parameters:

  • expected_price: u64
  • Merkle proof parameters
  • flags: u8

State: Same USDC transfer pattern as buy_pnft. cNFT transferred via Bubblegum.

accept_offer_for_pnft

Purpose: Seller accepts a buyer's offer for a pNFT

Signers: seller, rent_payer

Parameters: expected_price: u64

Key Accounts:

  • seller, buyer (validated against offer.buyer)
  • offer_rent_receiver, market, asset_id
  • seller/buyer NFT accounts, offer (closed)
  • usdc_mint, buyer_user_escrow_account
  • buyer_user_escrow_token_account
  • seller USDC account, treasury accounts
  • metadata accounts

Preconditions:

  • Marketplace not paused
  • seller ≠ buyer
  • offer.price == expected_price
  • Escrow token account balance ≥ tracked balance
  • Escrow has sufficient funds

State Transitions:

  1. Escrow balance decremented by price
  2. USDC price - fee transferred from escrow to seller
  3. USDC fee transferred from escrow to treasury
  4. pNFT transferred from seller to buyer (seller signs directly)
  5. Offer PDA closed

Important: Seller receives price - fee. Fee is deducted from offer amount.

Note: Whitelist is NOT re-validated at acceptance (validated at offer creation).

accept_offer_for_cnft

Purpose: Seller accepts a buyer's offer for an unlisted cNFT

Signers: seller, rent_payer

State: Same escrow-based USDC flow. cNFT transferred via Bubblegum; seller signs.

accept_offer_for_listed_cnft

Purpose: Seller accepts a buyer's offer for a cNFT that is currently listed

Signers: seller, rent_payer

Key Accounts: All accounts from accept_offer_for_cnft plus listing (closed) and listing_rent_receiver

State: Both Listing PDA and Offer PDA are closed. USDC flow from escrow. cNFT transferred via Bubblegum, signed by Listing PDA.


Offer Instructions

make_offer

Purpose: Create an offer using existing escrow funds

Signers: buyer, rent_payer

Parameters:

  • collection_type: CollectionType
  • collection_hash: [u8; 32]
  • price: u64

Preconditions:

  • Marketplace not paused
  • seller ≠ buyer
  • price ≥ market.min_offer_amount
  • Collection whitelisted (V2)
  • No existing offer for this (NFT, buyer) pair

State: Creates Offer PDA. Does NOT transfer USDC (requires pre-existing escrow balance).

Note: Escrow balance is not locked per-offer; checked at acceptance time.

make_offer_n_deposit

Purpose: Create an offer and deposit USDC into escrow in a single transaction

Signers: buyer, rent_payer

Parameters: Same as make_offer

State Transitions:

  1. USDC price transferred from buyer to escrow token account
  2. UserEscrow initialized if needed (idempotent)
  3. Escrow balance incremented
  4. Offer PDA created

Note: Preferred instruction for users without pre-existing escrow funds.

update_offer

Purpose: Change the price of an existing offer

Signers: buyer

Parameters: new_price: u64

Preconditions:

  • Marketplace not paused
  • new_price > min_offer_amount
  • new_price ≠ current_price
  • Offer exists
  • Escrow has sufficient funds for new_price

State: Updates offer.price and offer.updated_at

Note: No USDC movement. Escrow balance check is validation only, not a lock.

cancel_offer

Purpose: Cancel an offer, keeping funds in escrow

Signers: buyer

Parameters: force: bool (currently unused)

State: Closes Offer PDA; rent refunded to rent_receiver. USDC remains in escrow.

cancel_offer_n_withdraw

Purpose: Cancel an offer and withdraw funds from escrow

Signers: buyer

State Transitions:

  1. Escrow balance decremented by offer.price
  2. USDC transferred from escrow token account to buyer's USDC account
  3. Offer PDA closed

User Escrow Instructions

deposit_to_user_escrow

Purpose: Deposit USDC into the user's escrow for backing offers

Signers: user, rent_payer

Parameters: amount: u64

Key Accounts:

  • user, rent_payer, market, usdc_mint
  • user_escrow_account (init_if_needed PDA)
  • user_escrow_token_account (init_if_needed ATA)
  • user_usdc_account

State Transitions:

  1. UserEscrow initialized if first deposit (idempotent)
  2. USDC transferred from user to escrow token account
  3. Escrow balance incremented

withdraw_from_user_escrow

Purpose: Withdraw USDC from the user's escrow

Signers: user

Parameters: amount: u64

Preconditions:

  • Marketplace not paused
  • escrow.balance ≥ amount

State Transitions:

  1. Escrow balance decremented
  2. USDC transferred from escrow token account to user (signed by UserEscrow PDA)

Fee Model

Platform Fee Configuration

ParameterValue
Default200 bps (2.00%)
Minimum10 bps (0.10%)
Maximum1000 bps (10.00%)
Configurable Bysuper_admin via set_platform_fee
Stored Inmarket.royalty_fee

Fee Calculation

fee = floor(price * platform_fee_bps / 10000)

All arithmetic uses checked operations with u128 intermediate precision to prevent overflow.

Fee Flow by Transaction Type

Direct Purchase (buy_pnft / buy_cnft)

Buyer's USDC Account
|
|--- price ------> Seller's USDC Account
|--- fee --------> Treasury USDC Account
Offer PDA
const [offerPda] = PublicKey.findProgramAddressSync(
[Buffer.from("offer"), assetId.toBuffer(), buyer.toBuffer()],
PROGRAM_ID
);

// User Escrow PDA
const [userEscrowPda] = PublicKey.findProgramAddressSync(
[Buffer.from("user_escrow"), user.toBuffer()],
PROGRAM_ID
);

// Whitelist Entry PDA (V2)
const collectionTypeStr = collectionType === 'PNFT' ? 'PNFT' : 'CNFT';
const [whitelistEntryPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("whitelist_entry"),
marketPda.toBuffer(),
Buffer.from(collectionTypeStr),
collection.toBuffer(),
],
PROGRAM_ID
);

Integration Flows

Flow 1: List and Sell a pNFT

1. Seller calls list_pnft(price)
-> Listing PDA created
-> NFT delegated to Listing PDA

2. Buyer calls buy_pnft(expected_price)
-> USDC: buyer -> seller (price)
-> USDC: buyer -> treasury (fee)
-> NFT: seller -> buyer (via Listing PDA delegate)
-> Listing PDA closed

Flow 2: Make Offer and Accept

1. Buyer calls deposit_to_user_escrow(amount)
-> USDC moved to escrow

2. Buyer calls make_offer(collection_type, collection_hash, price)
-> Offer PDA created (no USDC movement)

OR: Buyer calls make_offer_n_deposit (combines steps 1+2)

3. Seller calls accept_offer_for_pnft(expected_price)
-> USDC: escrow -> seller (price - fee)
-> USDC: escrow -> treasury (fee)
-> NFT: seller -> buyer
-> Offer PDA closed

Flow 3: Accept Offer on a Listed cNFT

1. Seller has an active listing (list_cnft)
2. Buyer makes an offer (make_offer_n_deposit)
3. Seller calls accept_offer_for_listed_cnft(expected_price, ...)
-> Both Listing PDA and Offer PDA closed
-> USDC from escrow split between seller and treasury
-> cNFT transferred via Listing PDA (as delegate)

Using Anchor Client

import { Program, AnchorProvider } from '@coral-xyz/anchor';
import { Connection, PublicKey } from '@solana/web3.js';

// Connect to cluster
const connection = new Connection('https://api.mainnet-beta.solana.com');
const provider = new AnchorProvider(connection, wallet, {});
const program = new Program(IDL, PROGRAM_ID, provider);

// Example: List a pNFT
await program.methods
.listPnft(new BN(1000000)) // price in USDC lamports
.accounts({
seller: seller.publicKey,
rentPayer: rentPayer.publicKey,
market: marketPda,
whitelistEntry: whitelistEntryPda,
assetId: nftMint,
listing: listingPda,
// ... additional accounts
})
.signers([seller, rentPayer])
.rpc();

Transaction Size Considerations

cNFT operations require Merkle proofs passed via remaining_accounts. The proof size depends on tree depth and canopy:

proof_accounts_needed = depth - canopy

With default parameters (depth=20, canopy=14), this requires 6 proof accounts. Each remaining account adds ~32 bytes to the transaction.


Error Reference

CodeNameDescription
6000UnauthorizedSigner does not match required authority
6001CollectionAlreadyWhitelistedCollection is already in the whitelist (V1)
6002CollectionNotWhitelistedCollection is not whitelisted or whitelist entry does not match
6003InvalidRoyaltyFeeLegacy error — fee validation
6004InvalidMinOfferAmountMinimum offer amount must be > 0
6005MarketplacePausedOperation rejected — marketplace is paused
6006MarketplaceRunningCannot unpause — marketplace is already running
6007ListingAlreadyExistsA listing already exists for this (NFT, seller) pair
6008ListingNotExistsNo active listing found
6009ZeroPricePrice must be greater than 0
6010ArithmeticOverflowChecked arithmetic operation overflowed
6011InsufficientEscrowAmountEscrow amount insufficient
6012InsufficientLockedAmountLocked amount insufficient
6013OfferAlreadyExistsAn offer already exists for this (NFT, buyer) pair
6014OfferNotExistsNo active offer found (offer.price == 0)
6015InvalidOfferAmountOffer below minimum or equal to current price
6016OfferToOwnListingCannot make offer or trade with yourself
6017InvalidCNFTDepthDepth must be 1-32
6018InvalidCNFTCanopyCanopy must be 1-depth
6019InvalidCNFTProofSizeProof size out of valid range
6020InsufficientEscrowFundsEscrow balance insufficient for the operation
6021PriceMismatchOn-chain price does not match expected_price parameter
6022InvalidUsdcMintProvided mint does not match market.usdc_mint
6023InvalidProgramAddressExternal program ID does not match expected hardcoded value
6024EscrowBalanceMismatchTracked escrow balance exceeds actual token account balance
6025InvalidMerkleTreeOwnerMerkle tree not owned by SPL Account Compression
6026InvalidPlatformFeeFee must be between 10 and 1000 basis points

Common Failure Scenarios

ScenarioErrorResolution
Marketplace is pausedMarketplacePausedWait for admin to unpause
Collection not whitelistedCollectionNotWhitelistedContact admin or verify whitelist_entry PDA
Price changed between view and transactionPriceMismatchRe-fetch listing/offer price, rebuild transaction
Insufficient escrow balanceInsufficientEscrowFundsDeposit more USDC via deposit_to_user_escrow
Duplicate listingListingAlreadyExistsCancel existing listing first
Duplicate offerOfferAlreadyExistsCancel existing offer first
Escrow balance inconsistencyEscrowBalanceMismatchIndicates tracked balance > token account
Offer on own listingOfferToOwnListingCannot offer on your own NFT
Zero price listing/offerZeroPrice / InvalidOfferAmountPrice must be > 0

Security Considerations

PDA Protections

  • Deterministic derivation: All PDAs use fixed, predictable seeds with no user-controlled nonces
  • Bump stored on-chain: Each PDA stores its bump seed for consistent re-derivation
  • Listing as delegate authority: The Listing PDA serves as transfer delegate, ensuring only the program can authorize transfers

Frontrun / Race Condition Protection

Price matching: Both buy_* and accept_offer_for_* require an expected_price parameter. The instruction fails with PriceMismatch if the on-chain price differs, preventing:

  • Seller increasing listing price between buyer signature and transaction landing
  • Buyer decreasing offer between seller signature and transaction landing

Self-Trade Prevention

accept_offer_for_* and make_offer* enforce seller != buyer via the OfferToOwnListing error.

Escrow Balance Integrity

At offer acceptance, the handler checks:

buyer_user_escrow_token_account.amount >= user_escrow.balance

This detects inconsistency between tracked balance and actual token account. Failure raises EscrowBalanceMismatch.

USDC Mint Validation

Every USDC instruction validates the mint against market.usdc_mint using address constraints, preventing substitution of fake tokens.

External Program ID Validation

All external program accounts are validated via hardcoded address constraints:

  • MPL Token Metadata: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
  • Metaplex Bubblegum: BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY
  • SPL Account Compression: mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW
  • SPL Noop: mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3
  • MPL Token Auth Rules: auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg

Rent Payer Tracking

Listings and offers record who paid for the PDA rent (rent_payer field). On closure, rent is refunded to a rent_receiver account validated via:

address = listing.rent_payer

This ensures rent refunds go to the original payer.

Known Tradeoffs

TradeoffDescription
Escrowless listings - User responsibilityNFTs remain in seller wallets while listed. Sellers must manually cancel listings when transferring NFTs. The backend attempts to auto-cancel on transfer, but this is not 100% reliable. If an NFT is transferred away and later returned, the original listing may still be active.
Escrow not per-offer lockedA user's escrow balance backs all their offers collectively. If a user has 100 USDC in escrow and makes two 60 USDC offers, only one can be accepted. This is by design for capital efficiency.
Permissionless initializationAny wallet can call initialize. Safe due to singleton PDA constraint, but deployer must call promptly.
Direct buy fee is additiveBuyers pay price + fee, not price inclusive. Differs from offer model where fee is deducted.

External Program Dependencies

ProgramIDUsage
MPL Token MetadatametaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1spNFT delegation, transfer, revocation
Metaplex BubblegumBGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUYcNFT delegation, transfer
SPL Account Compressionmcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAWMerkle tree verification
SPL NoopmnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3Log wrapper for Bubblegum
MPL Token Auth Rulesauth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmggpNFT authorization rules
SPL Token(standard)USDC transfers
SPL Associated Token(standard)ATA creation/resolution
System Program(standard)Account creation, rent

Support

For technical questions, integration support, or to report issues, please contact the CollectorCrypt development team.