For the complete documentation index, see llms.txt. This page is also available as Markdown.

Website: Market

Step-by-step guide for web developers - show Market listings (every item for sale, a character's store) and sold listings (recent sales, price history, top sellers) on the website, with SQL, Node.js a

The Market keeps each character's store in one binary column and every sale in one table. This guide shows how to use both on a website: an all listings page, a character store page, a recent sales feed, price history and a sellers ladder.

What you can show

Page
Shows
Source

All listings

Every priced item on the server. Filter by currency, item, level. Sort by price or newest

Character.MarketStore

Character store

One character's priced rows

Character.MarketStore

My store (owner only)

All rows, unlisted rows and the wallet

Character.MarketStore

Recent sales

Last sales: item, seller, buyer, price, time

dbo.MarketSaleLog

Price history

Average / lowest sale price of an item

dbo.MarketSaleLog (or the archive)

Top sellers

Sales count and earnings per seller

The database does not store if a store is open or closed. A listing on the website means "this row has a price". The player buys it in game: by clicking the seller, or from the Market search (S key) when the store is open.

Where the data lives

Data
Where

Store rows + wallet

Character.MarketStore, varbinary(1536). NULL = empty store

Sales

dbo.MarketSaleLog, one row per sale

Item bytes

Same 16-byte format as Character.Inventory. Use your site's existing item decoder for names, options and images

Both are created by DataServer/SQL/Migrations/0002_market.sql (schema 2).

The store column is saved together with the inventory, right after each change (deposit, withdraw, open, close, sale, collect). A price change is saved within a few seconds.

Step 1 - Read the store blob

All numbers are little-endian (least significant byte first). Total size 1471 bytes.

Offset
Size
Field

0

1

Version. 1 = valid. 0 or NULL column = empty store

1

8

Wallet zen, signed 64-bit

9

4

Wallet Jewel of Bless (single jewels)

13

4

Wallet Jewel of Soul

17

4

Wallet Jewel of Chaos

21

29 × 50

50 rows. Row r starts at 21 + r × 29

Row (29 bytes):

Offset
Size
Field

0

16

Item, 16-byte DB format. All 0xFF = empty row

16

1

Currency. 0 = in the store but not listed

17

8

Price, signed 64-bit

25

4

Listed at, Unix time in seconds (UTC). Set when the price was set

Currency codes (same in MarketSaleLog.Currency):

Code
Currency

1

Zen

2

Jewel of Bless

3

Jewel of Soul

4

Jewel of Chaos

5

WCoinC

6

WCoinP

7

GoblinPoint

A row is for sale when the item is not empty, Currency is 1-7 and Price > 0. Jewel prices count single jewels.

Useful item bytes (offsets inside the 16-byte item):

Field
How

Section

byte9 >> 4

Number

byte0 + ((byte7 & 0x80) << 1)

Level (+0..+15)

(byte1 >> 3) & 15

Excellent options

byte7 & 63 (bit mask)

Durability or stack count

byte2

Serial

bytes 3-6, big-endian

Prices and zen are bigint. Zen can be above Number.MAX_SAFE_INTEGER, so convert with .toString() for JSON.

Needs 64-bit PHP 7.1+ (P = 64-bit little-endian). Read MarketStore with PDO::SQLSRV_ENCODING_BINARY so it comes back as raw bytes.

Step 2 - All listings page

Reading 1471 bytes per character in code is fine for one store. For a page that searches every store, add this view once. It turns each priced row into one SQL row, so you can filter, sort and page in SQL.

Run it against the MuOnline database in SQL Server Management Studio. It only adds a new object (WEB_ prefix); it changes nothing the game uses.

Rows with a price of 0 or below never get saved as listed, so Currency 1-7 is enough.

All listings, newest first (page 1, 20 per page):

Filter (zen only, +9 or higher, excellent, cheapest first):

One item type (section and number as in Item.xml):

Character store page:

Listings count per currency (stats box):

The view reads every character's store each time. That is fine for a few thousand characters. For a big server, cache the page for 30-60 seconds.

Step 3 - Sold listings

dbo.MarketSaleLog has one row per sale:

Column
Type
Meaning

Id

bigint, identity

Grows by 1 per row

Seller / Buyer

varchar(10)

Character names

ItemData

varbinary(16)

The item sold, 16-byte DB format

Currency

tinyint

Price

bigint

What the buyer paid

Net

bigint

What the seller got (price minus commission)

Status

tinyint

1 sold. 0 coin sale in progress, 2 refunded, 3 stuck coin sale (GM). Only show 1

SoldAt

datetime

Time of sale, SQL Server's local clock

Add this index once for time-based pages (safe, the game does not care):

Recent sales feed (last 20):

A character's sales (the in-game Sold tab):

A character's purchases:

Price history of one item type, last 30 days (zen):

SUBSTRING is 1-based, so item byte 9 is SUBSTRING(ItemData, 10, 1).

All-time sales archive

MarketSaleLog is a rolling window: each seller keeps only the last SaleLogKeep sold rows (default 50, set in Market.xml). Older ones are deleted. Short feeds are fine; all-time totals and long price history need their own table.

Use

Query MarketSaleLog directly?

Recent sales, a character's last sales, price history of busy items this week

Yes

All-time top sellers, total trade volume, long price history

No - use the archive

Install (once)

Run it once by hand to copy what is there now:

Keep it updated

Run EXEC dbo.WEB_MarketSalesUpdate every 1-5 minutes. Running it twice copies nothing twice.

Way
How

SQL Server Agent (not in Express)

New job, one step EXEC dbo.WEB_MarketSalesUpdate, schedule every 1 minute

Windows Task Scheduler

Every 1 minute: sqlcmd -S . -E -d MuOnline -Q "EXEC dbo.WEB_MarketSalesUpdate"

Website cron

Your site's scheduler runs the EXEC

Archive queries

Top sellers by zen earned:

Most sales (any currency): GROUP BY Seller ORDER BY COUNT(*) DESC.

Trade volume per day:

Price history: the price history query works on WEB_MarketSales too. Drop the Status = 1 line.

Season reset: TRUNCATE TABLE dbo.WEB_MarketSales.

Important Behavior

  • Open / closed is not stored. A listed row on the website may belong to a closed store. Do not show "buy" buttons: buying only happens in game.

  • Search is per GameServer in game. The website shows every character's store from the database, so it can list items a player on another server cannot reach with the search window.

  • Rows above MaxRows. If you lowered MaxRows in Market.xml, rows above it keep their price in the database but are not sold in game. Add AND [Row] < <MaxRows> to listing queries.

  • Rule changes. If an item stops being allowed (ItemMove, TradeItemBlock*, binding), the row becomes unlisted the next time the owner opens the store. Until then it still shows as listed.

  • Coin sales (Currency 5-7) appear in MarketSaleLog at Status = 0 first and turn to 1 when the item is delivered. Never count 0, 2 or 3.

  • Character rename. Old sale rows keep the old name.

  • Time. ListedAtUtc is UTC. SoldAt is the SQL Server machine's local time.

Common Issues

Last updated