MARKETPLACE 14 - EXPLICIT ACCESS AND EXACT ESCROW

Join Once, Then Trade Exact On-Chain Items

NiceChunk does not create market storage as a side effect of a listing or purchase. A wallet first reviews a live RPC quote and explicitly creates one 64-byte MarketUserState membership PDA. Listings then escrow one exact Backpack record, enforce at most 50 active listings per wallet, and settle SOL or NCK payment, item delivery, and listing state atomically through Market namespace 4 of the unified Game Program. The final design has no market-history PDA, expansion instruction, or History tab.

10 min read
In one game-anime coastal market mission, the canonical black-haired villager boy and canonical orange-haired villager girl face a central brass-and-wood escrow carriage holding exactly one cyan forged tool; two empty compartmented Backpacks stand open while physical seller and treasury payment trays divide coins beside Chunk.js-baked grass, stone, sand, plants, and depth-lit blue water.

Key points

Joining is a separate informed transaction

The market blocks trading until the wallet owner signs join_market. The game asks the configured RPC for current rent and transaction-fee values, shows each component, explains why the membership account exists, and links to this page before spending lamports.

One small membership account enforces the limit

MarketUserState is 64 bytes. It stores the owner, active-listing count, and last update slot. Creation stops at 50 active listings; a sale or cancellation decrements the seller count. No completed-trade references or paged history are stored.

Escrow and settlement preserve exact chain identity

A version-5 Listing stores the complete 80-byte Backpack slot. Purchase transfers the committed SOL or NCK price, applies the one-percent integer fee, appends those exact bytes to the buyer Backpack, and marks the Listing sold in one atomic instruction.

FIRST USE - REVIEW BEFORE SIGNING

Joining creates one personal PDA only after the wallet reviews a live quote

Opening Market derives MarketUserState from the connected wallet and checks it through the configured RPC. If the account is missing, the trading body becomes inert and a dedicated join card appears. The card shows MarketUserState rent, the estimated validator network fee, and the total. No listing query, purchase, cancellation, account creation, or wallet popup starts merely because the panel opened.

The owner-approved join transaction creates one 64-byte MarketUserState PDA. Its deterministic address uses the owner and configured Market Program. The account stores layout magic and version, PDA bump, active count, owner, update slot, and reserved zero bytes. It contains no trade references or page metadata.

Solana calls the lamports held by a rent-exempt account a minimum balance. In product terms it is an account-storage reserve, not revenue paid to NiceChunk. The current program does not expose a membership-close instruction, so players should treat the reserve as locked for now. The smaller network fee pays validators for processing join_market and is not recoverable. The one-percent market fee is different again and applies only when a Listing sells.

The UI obtains each number from getMinimumBalanceForRentExemption and getFeeForMessage. RPC policy and Solana rent parameters can change, so the wallet-facing quote is authoritative instead of a hardcoded documentation estimate.

Account 64-byte MarketUserState

Stores owner, active-listing count, update slot, and reserved final-layout bytes.

Account count One membership PDA

There is no MarketHistoryPage account or storage expansion transaction.

Active limit 50 listings per wallet

The same account is updated when listings are created, canceled, or sold.

EXACT INVENTORY CUSTODY

A listing escrows one complete slot and consumes one active-listing count

A Listing PDA is derived from the seller and a little-endian u64 listing ID. Creation requires a joined seller, a new uninitialized Listing address, fewer than 50 active listings, and a transferable source. The seller also funds the Listing account's own 216-byte rent reserve; that account is distinct from membership rent.

Backpack mode copies the selected seller-owned 80-byte slot, validates it, creates the Listing, and removes the exact dense record through the Backpack Program. Equipment mode resolves one of nine public Equipment PDA records and releases its custodied slot into the Listing. Unique Blueprint records are non-transferable. Solana instruction atomicity restores all source state if any later step fails.

The stored source slot preserves kind, quantity, block coordinates and ID or item identity, PDA, volume, mass metadata, durability, grade, level, quality, and metadata bytes. The client reconstructs the real material, block, or forged model from those fields. Search and category controls help navigation, but settlement follows the committed slot bytes.

Listing size 216 bytes

Version five keeps the exact 80-byte escrow slot, seller, price, source, timestamps, state, and buyer.

Escrow amount One complete dense slot

There is no partial quantity argument; the quantity encoded in the slot moves with it.

Transfer guard Unique Blueprint excluded

Market validation rejects the non-transferable Blueprint identity before inventory removal.

SOL OR NCK SETTLEMENT

Payment, Backpack delivery, Listing state, and seller count share one atomic boundary

Purchase validates an active version-5 Listing, its seller-scoped PDA, a different signing buyer, the selected currency route, the destination Backpack, MaterialPhysics, and both MarketUserState accounts. The seller membership is writable because settlement decrements its active count. The buyer membership is read-only because purchase only verifies that the buyer joined.

SOL transfers lamports to the seller and fixed treasury. NCK uses the configured mint, six decimals, validated token accounts, and transfer_checked. Both routes compute floor(price times 100 divided by 10,000), so the current market fee is one percent in base units and very small prices can round to zero fee. This settlement fee is unrelated to membership rent or validator network fees.

After payment, the MarketAuthority PDA signs a strict append of the exact escrow slot into the buyer Backpack. The Listing is marked sold with buyer, slot, and Unix time, then seller active_count decreases by one. If token, Backpack, authority, arithmetic, membership, or any later write fails, all transfers and program-account mutations roll back. A processed failed transaction may still consume its network fee.

Cancellation validates seller membership and a destination Backpack, returns the exact slot, marks the Listing canceled, and decrements active_count. Sold and canceled Listing PDAs currently remain allocated because the program has no Listing-close instruction, but the client creates no per-wallet history index and exposes no History view.

Currencies SOL and NCK

The buyer cannot override the price or currency committed by the seller's active Listing.

Settlement fee 100 basis points

Integer floor rounding is applied in the selected currency's base units.

Purchase membership Seller writable, buyer read-only

Both accounts must already exist, while only seller active_count changes.

BOUNDED RPC READS

The client reads active listings and shows authoritative item identities

Active listing discovery uses a Listing data-size filter plus optional state, seller, and currency filters. Browser pagination limits rendered rows. Loading states show a spinner rather than fake orders, while an RPC failure remains visible with a retry action.

Material, building, clothing, block, and forged categories derive from the committed source slot. Cards and details use the real Chunk.js item or forged model instead of a generic tool placeholder. Mass, cubic-centimeter volume, quality, durability, item identity, Listing PDA, and Explorer links come from decoded chain fields.

On mobile, Backpack and Listings are separate views rather than vertically compressed panels. Create, buy, cancel, and join controls enter a pending state during wallet and chain work, preventing accidental repeated submissions. Confirmed mutations refresh Backpack, Equipment, and active listings.

The final client does not load completed-trade pages, render a History tab, estimate history expansion, or submit an expansion instruction. This removes unnecessary RPC reads and account rent from normal market use.

Listing query Active 216-byte PDAs

The client filters program accounts and paginates the decoded result.

History query None

No history account, page read, history tab, or expansion action exists.

Mobile views Listings or Backpack

A tab switch gives each task the available screen instead of stacking both.

INDEPENDENT CHAIN CHECKS

Verify membership, escrow, settlement, and limits as separate facts

For membership, derive MarketUserState from the owner and configured Market Program. Verify the account owner, 64-byte length, magic, version, stored owner, active_count at or below 50, update slot, and zeroed reserved bytes. Compare the join transaction payer change with the pre-transaction rent minimum and actual network fee instead of treating the UI estimate as a receipt.

For listing creation, derive the seller-and-ID Listing PDA, verify version five and active state, compare the complete source slot with the seller Backpack or Equipment record removed in the same transaction, and confirm active_count increased by one without exceeding 50.

For purchase, verify the buyer differs from seller, the stored price and currency drove transfers, the one-percent split matches integer math, the buyer Backpack gained the exact escrow slot, the Listing names the buyer and sold timestamp, and seller active_count fell. For cancellation, verify exact return to a seller Backpack, canceled state, and the same decrement.

A submitted signature, thumbnail, local toast, category label, or fee estimate is supporting UI, not final proof. The strongest evidence is one confirmed transaction whose program-owned accounts and balances agree with every expected transition.

Membership proof One PDA plus payer delta

Use the actual 64-byte length, current rent minimum, and transaction metadata.

Escrow proof Same 80 bytes

Compare seller removal, Listing source slot, and buyer or seller destination append.

Limit proof active_count <= 50

Creation increments and successful settlement or cancellation decrements exactly once.

MEMBERSHIP, LIMIT, ADDRESS, AND SETTLEMENT EQUATIONS

Every market charge and state transition is independently checkable

Membership rent is separate from the validator network fee, Listing account rent, and the one-percent settlement fee. Deterministic PDA equations and exact slot bytes make each transition verifiable.

Explicit membership address and live join cost

P_user(o)=findProgramAddress([UTF8("market-user-v1"),o],G); joinCost=rent_RPC(64)+networkFee_RPC(joinTx)

G is the configured unified Game Program. join_market inner tag 3 creates only P_user, with the wallet as signer and payer. The game obtains both displayed lamport values from the active RPC immediately before confirmation. The 64-byte rent reserve remains locked because the current program has no membership-close instruction.

o
The wallet that owns, signs for, and funds its market membership account.
rent_RPC(64)
The current getMinimumBalanceForRentExemption result for the exact 64-byte account.
networkFee_RPC(joinTx)
The current getFeeForMessage result for the compiled join transaction.

Active-listing limit without history capacity

0<=active_count<=50; create: active_count'=active_count+1; cancel_or_sale: active_count'=active_count-1

MarketUserState rejects a creation once active_count reaches 50 and rejects a decrement from zero. There is no history_count, history_capacity, page counter, or expansion path in the final account layout.

active_count
The number of active Listing accounts currently owned by the seller wallet.
50
The fixed per-wallet ceiling that prevents market escrow from becoming unbounded inventory storage.

Listing identity and exact-slot escrow

P_listing(s,id)=findProgramAddress([UTF8("listing"),s,LE64(id)],G); Listing.len=216; escrowSlot=Backpack_before[index][0..80); Backpack_after=removeDense(Backpack_before,index)

Creation requires an existing seller membership, an owner signature, a new seller-scoped Listing PDA, and active_count below 50. Backpack and equipment sources both resolve to one complete validated 80-byte slot. The program stores that record before removing it from inventory; there is no partial-stack amount argument.

s, id
Seller public key and unsigned 64-bit seller-scoped Listing identifier.
escrowSlot
The exact block, material, or forged-item record committed by the program.
removeDense
Backpack removal that compacts later occupied slot indexes.

Atomic payment, delivery, and fee transition

fee=floor(price*100/10000); sellerAmount=price-fee; buy: activeListing+buyerSpace+bothMemberships -> soldListing+buyerSlot+sellerCountMinusOne

SOL and NCK settlement use the price and currency stored in the Listing. Account ownership, treasury, mint, token accounts, buyer Backpack space, and both membership PDAs are validated before completion. Solana atomicity rolls back payment, delivery, Listing state, and membership updates together when any instruction step fails, although a processed failed transaction can still consume its validator fee.

100/10000
The current compile-time one-percent marketplace settlement fee.
buyerSpace
One free destination Backpack record for the complete escrow slot.
bothMemberships
Existing seller and buyer MarketUserState PDAs; purchase never creates either account implicitly.

The final Market namespace exposes four instructions

Rust programs/nicechunk_market/src/lib.rs
    match tag {
        0 => create_listing(program_id, accounts, payload),
        1 => cancel_listing(program_id, accounts),
        2 => buy_listing(program_id, accounts),
        3 => join_market(program_id, accounts),
        _ => Err(NicechunkMarketError::InvalidInstruction.into()),
    }

Join is explicit. Create, cancel, and buy cannot hide membership account creation, and there is no history-expansion tag.

The membership account enforces the 50-listing ceiling

Rust programs/nicechunk_market/src/membership.rs
    pub fn increment_active(data: &mut [u8], updated_slot: u64) -> ProgramResult {
        let active_count = Self::active_count(data)?;
        if active_count >= MAX_ACTIVE_LISTINGS {
            return Err(NicechunkMarketError::ActiveListingLimitReached.into());
        }
        data[Self::ACTIVE_COUNT_OFFSET] = active_count + 1;
        Self::write_updated_slot(data, updated_slot)
    }

Every new listing consumes one seller membership count. The independent Listing PDA holds the escrowed item; MarketUserState does not copy item or completed-trade records.

The browser requests current membership rent and fee values

JavaScript src/chain/nicechunkChain.js
  const [userStateRentLamports, networkFeeLamports] = await Promise.all([
    conn.getMinimumBalanceForRentExemption(marketUserLength, "confirmed"),
    estimateMarketTransactionFeeLamports(conn, ownerKey, instruction),
  ]);
  const storageRentLamports = userStateRentLamports;

The quote follows the configured RPC and final 64-byte account length. The UI labels storage rent and validator network fee separately and does not treat either as the one-percent trading fee.

Purchase requires both existing membership PDAs

Rust programs/nicechunk_market/src/lib.rs
    validate_existing_market_user(program_id, seller_market_user, seller.key)?;
    validate_existing_market_user(program_id, buyer_market_user, buyer.key)?;
    let (seller_amount, fee_amount) = split_market_payment(price_base_units)?;

The buyer cannot be charged for a hidden membership allocation. Seller membership is later decremented, while buyer membership is validated read-only.

IMPLEMENTATION EVIDENCE

Where these claims come from

Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.

programs/nicechunk_market/src/membership.rs

Defines the final 64-byte MarketUserState, owner and update fields, reserved bytes, the 50-active limit, and checked increment and decrement operations.

programs/nicechunk_market/src/lib.rs

Routes create, cancel, buy, and explicit join tags; requires existing membership; escrows exact slots; settles SOL or NCK; and updates only seller active count after settlement.

programs/nicechunk_market/src/state.rs

Defines version-5 216-byte Listing records, complete 80-byte source slots, active, canceled, and sold states, price and currency fields, buyer identity, and settlement timestamps.

programs/nicechunk_market/src/errors.rs

Defines active-listing, market-user, already-joined, custody, payment, token, Backpack, and account validation failures without history-page errors.

src/chain/nicechunkChain.js

Derives and decodes the membership PDA, constructs final join, create, cancel, and buy account layouts, obtains live RPC rent and fee estimates, and rejects missing membership before submission.

play/play-market.js

Blocks first use behind an explicit membership quote, prevents duplicate operations, shows loading and retry states without placeholder listings, and reconstructs authoritative item details.

play/index.html

Provides the one-account membership cost card, rent explanation, rules link, accessible pending state, desktop terminal, and mobile Listings and Backpack views without a History tab.

scripts/update-locales.mjs

Supplies membership, rent, pending, error, and rules-link copy through the Play locale dictionaries without retired history-capacity strings.

play/tests/market-forged-chain-instruction.test.mjs

Checks exact unified join, Backpack listing, and equipment listing account layouts and validates the final 64-byte MarketUserState ABI.

play/tests/market-ui-browser.test.mjs

Exercises desktop and mobile Market rendering, real item icons and details, pending transaction controls, retryable RPC failures, explicit membership, and absence of retired history UI.