YAML Source is the Network Storage authoring standard.
Use the s&box Library Manager install for auto-updates. GitHub is best for agents, source review, contributions, and manual installs.

Player Marketplace System for s&box Games

Build a safe player-to-player marketplace with escrowed item listings. Sellers list items, Network Storage removes the listed quantity from the seller immediate...

# Player Marketplace System for s&box Games

Build a safe player-to-player marketplace with escrowed item listings. Sellers list items, Network Storage removes the listed quantity from the seller immediately, buyers pay the listing price, and the seller receives gold when the listing is bought.

This is the recommended starter pattern for auction houses, fish markets, trading posts, and resource exchanges.

## Collection: `market_players`

Create `collections/market_players.collection.yml`:

```yml
sourceVersion: "1"
kind: collection
name: market_players
description: Player currency and tradable item balances.
collectionType: per-steamid
accessMode: endpoint
maxRecords: 1
allowRecordDelete: false
requireSaveVersion: true
rateLimits:
mode: none
rateLimitAction: reject
schema:
type: object
properties:
gold:
type: number
min: 0
_ledger: true
items:
type: object
additionalProperties:
type: number
marketplaceSales:
type: number
min: 0
marketplacePurchases:
type: number
min: 0
```

## Collection: `market_listings`

Create `collections/market_listings.collection.yml`:

```yml
sourceVersion: "1"
kind: collection
name: market_listings
description: Global marketplace listings keyed by listing id.
collectionType: global
accessMode: endpoint
maxRecords: 10000
allowRecordDelete: false
requireSaveVersion: true
rateLimits:
mode: none
rateLimitAction: reject
schema:
type: object
properties:
listingId:
type: string
sellerSteamId:
type: string
itemId:
type: string
quantity:
type: number
min: 1
price:
type: number
min: 1
_ledger: true
status:
type: string
createdAtUnix:
type: number
buyerSteamId:
type: string
soldAtUnix:
type: number
```

## Game Values: listing limits

Create `collections/game_values.collection.yml`:

```yml
sourceVersion: "1"
kind: collection
name: game_values
description: Marketplace limits and allowed tradable item ids.
collectionType: game_values
accessMode: endpoint
schema: {}
constants:
- id: marketplace
name: Marketplace
entries:
min_price: 1
max_price: 1000000
max_quantity: 999
tables:
- id: tradable_items
name: Tradable Items
columns:
- key: id
type: string
- key: displayName
type: string
- key: enabled
type: boolean
rows:
- id: iron_ore
displayName: Iron Ore
enabled: true
- id: gold_ore
displayName: Gold Ore
enabled: true
- id: rare_fish
displayName: Rare Fish
enabled: true
```

## Workflow: validate listing creation

Create `workflows/market_validate_listing.workflow.yml`:

```yml
sourceVersion: "1"
kind: workflow
id: market_validate_listing
name: Market Validate Listing
description: Validates a seller-created marketplace listing.
params:
seller:
type: object
item:
type: object
itemId:
type: string
quantity:
type: number
price:
type: number
steps:
- id: item_enabled
type: condition
check:
all:
- field: "{{item.id}}"
op: exists
- field: "{{item.enabled}}"
op: "=="
value: true
onFail:
status: 400
errorCode: ITEM_NOT_TRADABLE
message: This item cannot be listed.
- id: safe_quantity
type: transform
expression: "clamp(floor({{quantity}}), 1, {{values.marketplace.max_quantity}})"
- id: safe_price
type: transform
expression: "clamp(floor({{price}}), {{values.marketplace.min_price}}, {{values.marketplace.max_price}})"
- id: seller_qty
type: transform
expression: "max(0, {{num(seller.items.{{itemId}}, 0)}})"
- id: has_items
type: condition
check:
field: "{{seller_qty}}"
op: ">="
value: "{{safe_quantity}}"
onFail:
status: 409
errorCode: NOT_ENOUGH_ITEMS
message: Seller does not have enough items.
returns:
quantity: "{{safe_quantity}}"
price: "{{safe_price}}"
```

## Endpoint: `create-market-listing`

Create `endpoints/create-market-listing.endpoint.yml`:

```yml
sourceVersion: "1"
kind: endpoint
name: Create Market Listing
slug: create-market-listing
method: POST
enabled: true
input:
type: object
properties:
listingId:
type: string
itemId:
type: string
quantity:
type: number
price:
type: number
required:
- listingId
- itemId
- quantity
- price
steps:
- id: seller
type: read
collection: market_players
key: "{{playerKey}}"
- id: existing
type: read
collection: market_listings
key: "{{input.listingId}}"
- id: listing_id_free
type: condition
check:
field: "{{existing.status}}"
op: not_exists
onFail:
status: 409
errorCode: LISTING_ID_EXISTS
message: Listing id already exists.
- id: item
type: lookup
source: values
table: tradable_items
where:
field: id
op: "=="
value: "{{input.itemId}}"
- id: listing
type: workflow
workflow: market_validate_listing
params:
seller: "{{seller}}"
item: "{{item}}"
itemId: "{{input.itemId}}"
quantity: "{{input.quantity}}"
price: "{{input.price}}"
- id: remove_quantity
type: transform
expression: "0 - {{listing.quantity}}"
- id: escrow
type: write
collection: market_players
key: "{{playerKey}}"
ops:
- op: inc
path: "items.{{input.itemId}}"
value: "{{remove_quantity}}"
- id: create_listing
type: write
collection: market_listings
key: "{{input.listingId}}"
ops:
- op: set
path: listingId
value: "{{input.listingId}}"
- op: set
path: sellerSteamId
value: "{{steamId}}"
- op: set
path: itemId
value: "{{input.itemId}}"
- op: set
path: quantity
value: "{{listing.quantity}}"
- op: set
path: price
value: "{{listing.price}}"
source: marketplace
reason: Listing created
- op: set
path: status
value: open
- op: set
path: createdAtUnix
value: "{{_unixS}}"
response:
status: 200
body:
ok: true
listingId: "{{input.listingId}}"
itemId: "{{input.itemId}}"
quantity: "{{listing.quantity}}"
price: "{{listing.price}}"
```

## Endpoint: `buy-market-listing`

Create `endpoints/buy-market-listing.endpoint.yml`:

```yml
sourceVersion: "1"
kind: endpoint
name: Buy Market Listing
slug: buy-market-listing
method: POST
enabled: true
input:
type: object
properties:
listingId:
type: string
required:
- listingId
steps:
- id: listing
type: read
collection: market_listings
key: "{{input.listingId}}"
- id: listing_open
type: condition
check:
field: "{{listing.status}}"
op: "=="
value: open
onFail:
status: 404
errorCode: LISTING_NOT_OPEN
message: Listing is not available.
- id: not_own_listing
type: condition
check:
field: "{{listing.sellerSteamId}}"
op: "!="
value: "{{steamId}}"
onFail:
status: 409
errorCode: CANNOT_BUY_OWN_LISTING
- id: buyer
type: read
collection: market_players
key: "{{playerKey}}"
- id: seller
type: read
collection: market_players
key: "{{listing.sellerSteamId}}"
- id: can_afford
type: condition
check:
field: "{{buyer.gold}}"
op: ">="
value: "{{listing.price}}"
onFail:
status: 402
errorCode: NOT_ENOUGH_GOLD
message: Buyer does not have enough gold.
- id: spend
type: transform
expression: "0 - {{listing.price}}"
- id: mark_sold
type: write
collection: market_listings
key: "{{input.listingId}}"
ops:
- op: set
path: status
value: sold
- op: set
path: buyerSteamId
value: "{{steamId}}"
- op: set
path: soldAtUnix
value: "{{_unixS}}"
- id: debit_buyer
type: write
collection: market_players
key: "{{playerKey}}"
ops:
- op: inc
path: gold
value: "{{spend}}"
source: marketplace
reason: "Bought listing {{input.listingId}}"
- op: inc
path: "items.{{listing.itemId}}"
value: "{{listing.quantity}}"
- op: inc
path: marketplacePurchases
value: 1
- id: credit_seller
type: write
collection: market_players
key: "{{listing.sellerSteamId}}"
ops:
- op: inc
path: gold
value: "{{listing.price}}"
source: marketplace
reason: "Sold listing {{input.listingId}}"
- op: inc
path: marketplaceSales
value: 1
response:
status: 200
body:
ok: true
listingId: "{{input.listingId}}"
itemId: "{{listing.itemId}}"
quantity: "{{listing.quantity}}"
price: "{{listing.price}}"
sellerSteamId: "{{listing.sellerSteamId}}"
```

## C# calls from s&box

```csharp
var listingId = $"list_{Guid.NewGuid():N}";
await NetworkStorage.CallEndpoint( "create-market-listing", new
{
listingId,
itemId = "iron_ore",
quantity = 100,
price = 750
} );

await NetworkStorage.CallEndpoint( "buy-market-listing", new
{
listingId
} );
```

## Production notes

- Use random, high-entropy listing ids from the game server or client UI to avoid collisions.
- Keep listing creation public only if the item is immediately escrowed, as shown here.
- For very high-volume markets, run purchases through a dedicated-server order matcher and keep `buy-market-listing` secret-key gated.

## Recommended rate limits

| Rule | Collection | Field | Scope | Window | Limit | Action |
|------|------------|-------|-------|--------|-------|--------|
| `market_listing_create` | `market_listings` | `price` | per_player | perMinute | 30 | reject |
| `market_gold_flow` | `market_players` | `gold` | per_player | perHour | 1000000 | flag |

This example uses escrow instead of trusting the seller to still own the item later, which prevents duplicate-selling and most simple market exploits.