Tokenized Models
Introduction
Tokenized models combine machine learning with DeFi primitives on PandaChain. A TokenizedModel wraps an on-chain ML model with a PRC-20 token backed by a quadratic bonding curve. Token holders get free inference access and a pro-rata share of inference revenue (dividends). Tokens can be listed and traded on the ModelExchange order book with OHLC charting, and bundled into a NAV-based ModelIndex fund.
This guide covers:
- The
TokenizedModelcontract (ML model + bonding-curve token + dividends) - Bonding curve pricing mechanics
- Dividend distribution from inference revenue
- The
ModelExchange(order book, OHLC, volume, trending) - The
ModelIndex(NAV index fund of model tokens)
All three contracts ship in the contracts/marketplace/ directory: tokenized_model.py, model_exchange.py, and model_index.py.
All prices, payments, balances, and NAV are integers in base units. PandaChain contracts never use floating-point money. Bonding-curve math multiplies before it divides to stay deterministic.
TokenizedModel Concept
A TokenizedModel is a single contract that combines:
- An ML model — a pre-trained model serialized to a dict (must contain the
"__panda_ml_model__"key), loadable viapanda.ml.load_model. - A PRC-20 token — a
FungibleToken(18 decimals). On deploy, 60% oftotal_supplyis minted to the creator and the remaining 40% is sold through the bonding curve. - Dividend distribution — inference fees from non-holders accumulate as
total_revenueand are claimable pro-rata by token holders.
When users buy tokens, the bonding curve mints new tokens at an increasing price. When they sell, the curve burns tokens and pays out from the reserve. This creates automatic price discovery based on demand.
Constructor
@constructor
def deploy(
self,
ctx,
model_name: str,
model_dict: dict,
token_name: str,
token_symbol: str,
total_supply: int,
base_price: int,
inference_fee: int = 0,
description: str = "",
):
...
model_dictmust contain"__panda_ml_model__"or the deploy reverts. The model is loaded once at deploy to validate it.total_supplyandbase_pricemust be positive integers.- 60% (
6000bps) is minted toctx.sender(the creator); 40% (4000bps) becomes the curve supply. - An initial OHLC candle is seeded at
base_price. - Emits
ModelTokenized(model_name, token_name, token_symbol, total_supply, creator).
Bonding-curve token operations
@call
def buy_tokens(self, ctx, amount: int, payment: int = 0):
"""Buy `amount` tokens from the curve. `payment` is the PANDA attached (an int arg)."""
@call
def sell_tokens(self, ctx, amount: int):
"""Sell `amount` tokens back to the curve for PANDA from the reserve."""
buy_tokensprices the purchase on the curve, requirespayment >= cost, mints the tokens, growscurve_soldandcurve_reserve, records the price, and emitsTokensBought(buyer, amount, cost, price).sell_tokensburns the seller's tokens, pays out from the reserve (capped at the reserve balance), shrinkscurve_soldandcurve_reserve, and emitsTokensSold(seller, amount, payout, price).
Inference
@call
def predict(self, ctx, x: list, payment: int = 0) -> list:
"""Run inference. Token holders (balance > 0) pay nothing.
Non-holders must attach payment >= inference_fee."""
- Holders call for free. Non-holders pay
inference_fee; the payment is added tototal_revenue. - Returns the model's predictions and emits
Inference(caller, is_holder, fee_paid).
Dividends
@call
def claim_dividends(self, ctx):
"""Claim accumulated share of inference revenue."""
Each holder's owed amount is computed cumulatively:
owed = total_revenue * balance // total_supply - already_claimed
claim_dividends records the claim and emits DividendClaimed(claimer, amount). There is no separate transfer step in the contract — the claim accounting is the settlement record.
Model governance
The creator can retrain directly; holders can govern updates by vote:
@call
def train(self, ctx, new_model_dict: dict):
"""Creator-only direct model swap. Emits ModelUpdated."""
@call
def propose_update(self, ctx, new_model_dict: dict, description: str = "") -> int:
"""Any holder proposes a new model. Returns the proposal id. Emits UpdateProposed."""
@call
def vote_update(self, ctx, proposal_id: int, support: bool):
"""Vote on a proposal; vote weight = token balance. Emits VoteCast."""
@call
def execute_update(self, ctx, proposal_id: int):
"""Apply a passed proposal (votes_for > votes_against). Emits ProposalExecuted."""
Queries
| Query | Returns |
|---|---|
get_price() | Current bonding-curve price (int) |
get_ohlc(start_epoch=0, end_epoch=0) | List of candles {epoch, open, high, low, close} |
get_stats() | Model + financial metrics (name, type, supply, curve, revenue, fee, …) |
get_holders() | Holder list [{address, balance}], sorted by balance |
balance_of(owner) | Token balance (int) |
dividends_owed(addr) | Unclaimed dividends (int) |
get_proposal(proposal_id) | Proposal dict |
SDK aliases
For the polymorphic panda.model SDK, TokenizedModel also exposes metadata() (canonical model metadata with kind="tokenized"), inference(x, payment=0) (alias of predict), buy(amount, payment=0) (alias of buy_tokens), and get_holdings(owner="") (returns {owner, balance}).
Bonding Curve Pricing
The bonding curve is quadratic in the fraction of the curve sold:
price = base_price * (curve_sold / curve_supply) ** 2
Implemented with integer math (multiply first, then divide, with a floor of 1):
def _bonding_price(base_price: int, sold: int, total: int) -> int:
if total == 0:
return base_price
return max(1, base_price * sold * sold // (total * total))
This means:
- Early buyers get tokens cheaply (price approaches
base_price * (sold/supply)^2). - Price rises quadratically as more of the curve supply is sold.
- Selling returns PANDA from the reserve at the curve price, capped at the available reserve.
- No external liquidity is needed — the curve itself acts as the market maker.
The cost of a buy_tokens call is price * amount, where price is computed at the new curve_sold level. OHLC candles cover 100 blocks each (one epoch).
How Dividends Work
Every time a non-holder pays for inference, the fee accrues to total_revenue. Holders can then claim their pro-rata share:
- Total owed to a holder is
total_revenue * balance // total_supply(integer math). - The contract tracks
claimed[addr]— the cumulative amount already claimed. claim_dividends()pays outowed = total_owed - already_claimedand records it.- Holders accrue passive income proportional to their holdings, funded by real model usage.
Use the dividends_owed(addr) query to preview a holder's claimable amount before calling.
The ModelExchange
The ModelExchange is a central order-book DEX for tokenized model tokens. It does not custody tokens — it records orders, matches crossing orders by price-time priority, and tracks OHLC, volume, and trending stats. Listings and orders are keyed by integer ids (not addresses).
@constructor
def deploy(self, ctx):
"""No constructor arguments. Deployer becomes the exchange owner."""
Listing
@call
def list_model_token(
self, ctx, model_contract: str, token_symbol: str,
token_name: str = "", description: str = "",
) -> int:
"""Register a model token for trading. Returns the integer listing_id.
Emits ModelListed. Reverts if the model_contract is already listed."""
@call
def delist_model_token(self, ctx, listing_id: int):
"""Deactivate a listing. Only the lister or exchange owner. Emits ModelDelisted."""
Orders
@call
def place_order(self, ctx, listing_id: int, side: str, price: int, amount: int) -> int:
"""Place a limit order. side is "buy" or "sell"; price and amount are ints.
Auto-matches against resting opposite orders, then emits OrderPlaced.
Returns the integer order_id."""
@call
def cancel_order(self, ctx, order_id: int):
"""Cancel a resting order. Only the order's trader. Emits OrderCancelled."""
Matching uses price-time priority: a new order is filled against compatible resting orders at the maker's price. Each fill emits a Trade(listing_id, price, amount, buyer, seller) event and updates OHLC, volume, total volume, last price, and trade count for the listing.
Queries
| Query | Returns |
|---|---|
get_listing(listing_id) | Listing dict |
get_all_listings() | All active listings [{listing_id, ...}] |
get_all_models() | Active listings shaped for the panda.model SDK (address/model_contract/model_address aliased) |
get_ohlc(listing_id, start_epoch=0, end_epoch=0) | Candles {epoch, open, high, low, close} |
get_orderbook(listing_id) | {"buys": [...], "sells": [...]} with remaining amounts |
get_volume(listing_id, start_epoch=0, end_epoch=0) | {listing_id, total_volume, per_epoch} |
get_trending(top_k=10) | Top listings by total volume |
get_order(order_id) | Order dict |
The orderbook returns buys sorted by descending price and sells by ascending price; each entry is {order_id, trader, price, amount} where amount is the unfilled remainder.
ModelIndex (NAV Index Fund)
The ModelIndex is a diversified index fund over tokenized model tokens. Investors buy index tokens (a PRC-20 FungibleToken) priced by net asset value (NAV). Constituents carry a relative weight and a performance score; rebalance() reallocates the fund's assets proportional to weight * score. Constituents are keyed by integer ids.
@constructor
def deploy(self, ctx, index_name: str, index_symbol: str, initial_supply: int = 0):
"""Deploy the fund. Deployer becomes the manager. Emits IndexCreated.
Optionally mints initial_supply index tokens to the manager."""
Constituent management (manager only)
@call
def add_constituent(self, ctx, model_contract: str, token_symbol: str,
weight: int = 100, initial_score: int = 100) -> int:
"""Add a model token to the index. Returns the integer constituent_id.
Emits ConstituentAdded."""
@call
def remove_constituent(self, ctx, constituent_id: int):
"""Deactivate a constituent (weight set to 0). Emits ConstituentRemoved."""
@call
def update_score(self, ctx, constituent_id: int, new_score: int):
"""Update a constituent's performance score. Emits ScoreUpdated."""
Rebalance and trading
@call
def rebalance(self, ctx):
"""Anyone can trigger. Reallocates total_assets proportional to
weight * score across active constituents, records NAV, emits Rebalanced."""
@call
def buy_index(self, ctx, amount: int, payment: int = 0):
"""Deposit `amount` PANDA (payment >= amount) and receive index tokens
proportional to NAV. Emits IndexBought."""
@call
def sell_index(self, ctx, token_amount: int):
"""Burn index tokens and withdraw proportional PANDA. Emits IndexSold."""
NAV per token is computed with integer math:
nav_per_token = total_assets * 10**18 // index_supply
On buy_index, when the fund is empty the first deposit mints 1:1; otherwise tokens_to_mint = amount * supply // total_assets. On sell_index, payout = token_amount * total_assets // supply.
Queries
| Query | Returns |
|---|---|
get_constituents() | Active constituents [{constituent_id, weight, score, allocation, ...}] |
get_nav() | {total_assets, total_supply, nav_per_token, rebalance_count, last_rebalance_block} |
get_nav_history() | Historical NAV per rebalance epoch |
balance_of(owner) | Index-token balance (int) |
index_info() | {name, symbol, manager, total_supply, total_assets, num_constituents, rebalance_count} |
get_composite_performance() | Weighted-average score across active constituents |
Full Code Example
Deploy and trade a tokenized model
The real client is PandaProvider from the panda-sdk-client package. deploy_file() deploys a contract straight from its .py file plus a constructor_args dict (use deploy(code, ...) if you already have the source string); call() takes the method name and an args dict (payments are integer arguments like payment, not a separate value= field); query() is read-only and returns the method's value directly.
from panda_client import PandaProvider
provider = PandaProvider("http://localhost:8545", sender="0xCreator")
# 1. Deploy the TokenizedModel straight from its source file.
model_dict = {
"__panda_ml_model__": "LinearRegression",
"coef": [2.0],
"intercept": 1.0,
}
dep = provider.deploy_file(
"contracts/marketplace/tokenized_model.py",
constructor_args={
"model_name": "FraudNet",
"model_dict": model_dict,
"token_name": "FraudNet Token",
"token_symbol": "FNET",
"total_supply": 10000,
"base_price": 100,
"inference_fee": 50,
"description": "Fraud detector",
},
)
model_addr = dep.contract_address
# 2. Buy tokens via the bonding curve. `payment` is an integer arg in base units.
provider.call(
model_addr,
"buy_tokens",
{"amount": 100, "payment": 1_000_000},
sender="0xBuyer",
)
# 3. Run inference. The buyer now holds tokens, so inference is free.
preds = provider.call(model_addr, "predict", {"x": [[1], [2]], "payment": 0}, sender="0xBuyer")
# 4. A non-holder pays the inference fee, which accrues to dividends.
provider.call(model_addr, "predict", {"x": [[3]], "payment": 50}, sender="0xUser")
# 5. Claim dividends as a token holder.
provider.call(model_addr, "claim_dividends", {}, sender="0xBuyer")
# 6. Read the current price and stats (read-only, no transaction).
price = provider.query(model_addr, "get_price") # int
stats = provider.query(model_addr, "get_stats") # dict
owed = provider.query(model_addr, "dividends_owed", {"addr": "0xBuyer"})
# 7. Sell tokens back to the curve.
provider.call(model_addr, "sell_tokens", {"amount": 50}, sender="0xBuyer")
List and trade on the ModelExchange
# Deploy the exchange (no constructor args).
ex = provider.deploy_file("contracts/marketplace/model_exchange.py",
constructor_args={}, sender="0xOwner")
ex_addr = ex.contract_address
# List the model token; the call returns an integer listing_id via its receipt.
provider.call(
ex_addr,
"list_model_token",
{"model_contract": model_addr, "token_symbol": "FNET", "token_name": "FraudNet Token"},
sender="0xAlice",
)
# Place crossing orders (listing_id is an integer). They auto-match.
provider.call(ex_addr, "place_order", {"listing_id": 1, "side": "sell", "price": 100, "amount": 20}, sender="0xSeller")
provider.call(ex_addr, "place_order", {"listing_id": 1, "side": "buy", "price": 100, "amount": 20}, sender="0xBuyer")
# Inspect the book and OHLC.
book = provider.query(ex_addr, "get_orderbook", {"listing_id": 1}) # {"buys": [...], "sells": [...]}
candles = provider.query(ex_addr, "get_ohlc", {"listing_id": 1})