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 TokenizedModel contract (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:

  1. An ML model — a pre-trained model serialized to a dict (must contain the "__panda_ml_model__" key), loadable via panda.ml.load_model.
  2. A PRC-20 token — a FungibleToken (18 decimals). On deploy, 60% of total_supply is minted to the creator and the remaining 40% is sold through the bonding curve.
  3. Dividend distribution — inference fees from non-holders accumulate as total_revenue and 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_dict must contain "__panda_ml_model__" or the deploy reverts. The model is loaded once at deploy to validate it.
  • total_supply and base_price must be positive integers.
  • 60% (6000 bps) is minted to ctx.sender (the creator); 40% (4000 bps) 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_tokens prices the purchase on the curve, requires payment >= cost, mints the tokens, grows curve_sold and curve_reserve, records the price, and emits TokensBought(buyer, amount, cost, price).
  • sell_tokens burns the seller's tokens, pays out from the reserve (capped at the reserve balance), shrinks curve_sold and curve_reserve, and emits TokensSold(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 to total_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

QueryReturns
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:

  1. Total owed to a holder is total_revenue * balance // total_supply (integer math).
  2. The contract tracks claimed[addr] — the cumulative amount already claimed.
  3. claim_dividends() pays out owed = total_owed - already_claimed and records it.
  4. 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

QueryReturns
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

QueryReturns
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})