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 bonding curve. Token holders earn dividends from inference revenue, and tokens can be traded on the ModelExchange with full order book and OHLC charting.
This guide covers:
- The TokenizedModel contract (ML model + bonding curve token)
- Bonding curve pricing mechanics
- Dividend distribution from inference revenue
- The ModelExchange (order book, OHLC data)
- The ModelIndex (index fund of model tokens)
TokenizedModel Concept
A TokenizedModel is a single contract that combines:
- An ML model -- trained on-chain using
panda.ml, stored in contract state - A PRC-20 token -- minted via a bonding curve, representing ownership shares
- Dividend distribution -- inference fees are split proportionally among token holders
When users buy tokens, the bonding curve mints new tokens at an increasing price. When they sell, the curve burns tokens. This creates automatic price discovery based on demand.
Full TokenizedModel Contract
"""
TokenizedModel -- ML model with integrated PRC-20 token and bonding curve.
Demonstrates:
- Bonding curve token issuance (price = supply^2 / scale)
- Dividend distribution from inference revenue
- Combined ML training + DeFi token economics
"""
from panda import contract, constructor, call, query, event
from panda.ml import LinearRegression, save_model, load_model, r2_score
import math
@contract
class TokenizedModel:
"""ML model with bonding curve token and inference dividends."""
class State:
# Model metadata
owner: str = ""
name: str = ""
symbol: str = ""
category: str = "ML"
description: str = ""
# ML model
model: dict = {}
is_trained: bool = False
accuracy: float = 0.0
total_inferences: int = 0
# Token state (PRC-20 compatible)
total_supply: int = 0
balances: dict = {}
allowances: dict = {}
# Bonding curve parameters
curve_scale: float = 1000000.0
reserve_balance: float = 0.0
# Dividends
total_revenue: float = 0.0
dividend_per_token: float = 0.0
claimed: dict = {}
@constructor
def deploy(self, ctx, name: str, symbol: str, category: str = "ML", curve_scale: float = 1000000.0):
self.state.owner = ctx.sender
self.state.name = name
self.state.symbol = symbol
self.state.category = category
self.state.curve_scale = curve_scale
self.emit(event.TokenizedModelDeployed(name=name, symbol=symbol, owner=ctx.sender))
# -- Bonding Curve --
def _get_price(self) -> float:
"""Current token price from bonding curve: price = supply^2 / scale."""
s = self.state.total_supply
return (s * s) / self.state.curve_scale if s > 0 else 1.0 / self.state.curve_scale
def _cost_to_buy(self, amount: int) -> float:
"""Integral of bonding curve from current supply to supply + amount."""
s = self.state.total_supply
# Integral of x^2/scale from s to s+amount = (1/scale) * [(s+a)^3 - s^3] / 3
new_s = s + amount
return ((new_s ** 3) - (s ** 3)) / (3 * self.state.curve_scale)
def _return_for_sell(self, amount: int) -> float:
"""Integral of bonding curve from supply - amount to supply."""
s = self.state.total_supply
new_s = s - amount
return ((s ** 3) - (new_s ** 3)) / (3 * self.state.curve_scale)
@call
def buy(self, ctx, amount: int):
"""Buy tokens via bonding curve. Send PANDA >= cost."""
cost = self._cost_to_buy(amount)
if ctx.value < cost:
raise ValueError(f"Insufficient payment: need {cost}, got {ctx.value}")
self.state.total_supply += amount
self.state.balances[ctx.sender] = self.state.balances.get(ctx.sender, 0) + amount
self.state.reserve_balance += cost
# Refund excess
excess = ctx.value - cost
if excess > 0:
ctx.transfer(ctx.sender, excess)
self.emit(event.TokensBought(buyer=ctx.sender, amount=amount, cost=cost))
@call
def sell(self, ctx, amount: int):
"""Sell tokens back to bonding curve."""
bal = self.state.balances.get(ctx.sender, 0)
if bal < amount:
raise ValueError("Insufficient balance")
payout = self._return_for_sell(amount)
self.state.total_supply -= amount
self.state.balances[ctx.sender] = bal - amount
self.state.reserve_balance -= payout
ctx.transfer(ctx.sender, payout)
self.emit(event.TokensSold(seller=ctx.sender, amount=amount, payout=payout))
# -- ML Model --
@call
def train(self, ctx, features: list, labels: list):
"""Train the model. Owner only."""
if ctx.sender != self.state.owner:
raise ValueError("Only owner can train")
model = LinearRegression()
model.fit(features, labels)
preds = model.predict(features)
r2 = r2_score(labels, preds)
self.state.model = save_model(model)
self.state.is_trained = True
self.state.accuracy = round(r2 * 100, 2)
self.emit(event.ModelTrained(accuracy=self.state.accuracy))
@call
def predict(self, ctx, features: list) -> list:
"""Run inference. Pays into dividend pool."""
if not self.state.is_trained:
raise ValueError("Model not trained")
model = load_model(self.state.model)
predictions = model.predict(features)
# Add inference fee to dividend pool
fee = ctx.value
if fee > 0 and self.state.total_supply > 0:
self.state.total_revenue += fee
self.state.dividend_per_token += fee / self.state.total_supply
self.state.total_inferences += 1
return [round(p, 4) for p in predictions]
# -- Dividends --
@call
def claim_dividends(self, ctx):
"""Claim accumulated dividends."""
bal = self.state.balances.get(ctx.sender, 0)
if bal <= 0:
raise ValueError("No tokens held")
last_claimed = self.state.claimed.get(ctx.sender, 0.0)
owed = bal * (self.state.dividend_per_token - last_claimed)
if owed <= 0:
raise ValueError("Nothing to claim")
self.state.claimed[ctx.sender] = self.state.dividend_per_token
ctx.transfer(ctx.sender, owed)
self.emit(event.DividendsClaimed(holder=ctx.sender, amount=owed))
# -- Queries --
@query
def get_price(self) -> float:
return self._get_price()
@query
def get_stats(self) -> dict:
return {
"name": self.state.name,
"symbol": self.state.symbol,
"category": self.state.category,
"accuracy": self.state.accuracy,
"totalInferences": self.state.total_inferences,
"price": self._get_price(),
"totalSupply": self.state.total_supply,
"totalRevenue": self.state.total_revenue,
}
Bonding Curve Pricing
The bonding curve uses a quadratic formula:
price(supply) = supply^2 / curve_scale
This means:
- Early buyers get tokens cheaply
- Price increases as more tokens are minted
- Selling returns PANDA based on the integral (area under the curve)
- No external liquidity is needed -- the curve itself acts as the market maker
The curve_scale parameter controls the steepness. A higher scale means a flatter curve (slower price increase).
Cost to Buy
The cost to buy n tokens at supply s is the integral of the price function:
cost = integral from s to s+n of (x^2 / scale) dx
= [(s+n)^3 - s^3] / (3 * scale)
Return for Selling
The return for selling n tokens at supply s is:
return = [s^3 - (s-n)^3] / (3 * scale)
How Dividends Work
Every time someone pays for inference (calls predict() with a payment), the fee is distributed across all token holders:
- The fee is divided by
total_supplyto get the per-token dividend increment dividend_per_tokenis a cumulative counter that only increases- When a holder calls
claim_dividends(), they receive:owed = balance * (current_dividend_per_token - last_claimed_per_token) - This is the standard "dividend distribution" pattern used in DeFi
Token holders earn passive income proportional to their holdings, funded by actual model usage.
The ModelExchange
The ModelExchange is a central exchange contract where tokenized model tokens can be traded with an order book:
"""
ModelExchange -- Central exchange for trading model tokens.
Features:
- Limit order book (buy/sell orders)
- Order matching engine
- OHLC candlestick data generation
- Model registry and discovery
"""
from panda import contract, constructor, call, query, event
@contract
class ModelExchange:
class State:
admin: str = ""
models: dict = {} # address -> model metadata
orders: dict = {} # order_id -> order
next_order_id: int = 1
ohlc: dict = {} # address -> list of candles
trades: dict = {} # address -> list of trades
@constructor
def deploy(self, ctx):
self.state.admin = ctx.sender
@call
def list_model(self, ctx, model_address: str, name: str, symbol: str, category: str = "ML"):
"""Register a TokenizedModel on the exchange."""
self.state.models[model_address] = {
"name": name, "symbol": symbol, "category": category,
"listed_by": ctx.sender, "listed_at_block": ctx.block_number,
}
self.emit(event.ModelListed(address=model_address, name=name))
@call
def place_order(self, ctx, model_address: str, side: str, price: float, amount: int):
"""Place a buy or sell limit order."""
order_id = self.state.next_order_id
self.state.next_order_id += 1
self.state.orders[order_id] = {
"id": order_id, "model": model_address, "side": side,
"price": price, "amount": amount, "filled": 0,
"trader": ctx.sender, "block": ctx.block_number,
}
self._try_match(model_address)
self.emit(event.OrderPlaced(order_id=order_id, side=side, price=price, amount=amount))
def _try_match(self, model_address: str):
"""Simple matching engine: match crossing orders."""
buys = sorted(
[o for o in self.state.orders.values()
if o["model"] == model_address and o["side"] == "buy" and o["amount"] > o["filled"]],
key=lambda o: -o["price"]
)
sells = sorted(
[o for o in self.state.orders.values()
if o["model"] == model_address and o["side"] == "sell" and o["amount"] > o["filled"]],
key=lambda o: o["price"]
)
for buy in buys:
for sell in sells:
if buy["price"] >= sell["price"]:
qty = min(buy["amount"] - buy["filled"], sell["amount"] - sell["filled"])
if qty > 0:
buy["filled"] += qty
sell["filled"] += qty
self._record_trade(model_address, sell["price"], qty)
def _record_trade(self, model_address: str, price: float, amount: int):
if model_address not in self.state.trades:
self.state.trades[model_address] = []
self.state.trades[model_address].append({"price": price, "amount": amount})
@query
def get_orderbook(self, model_address: str) -> dict:
bids = [o for o in self.state.orders.values()
if o["model"] == model_address and o["side"] == "buy" and o["amount"] > o["filled"]]
asks = [o for o in self.state.orders.values()
if o["model"] == model_address and o["side"] == "sell" and o["amount"] > o["filled"]]
return {"bids": sorted(bids, key=lambda o: -o["price"]),
"asks": sorted(asks, key=lambda o: o["price"])}
@query
def get_ohlc(self, model_address: str, interval: str = "1H", limit: int = 100) -> list:
return self.state.ohlc.get(model_address, [])[-limit:]
@query
def get_all_models(self) -> list:
return [{"address": addr, **meta} for addr, meta in self.state.models.items()]
ModelIndex (Index Fund)
The ModelIndex contract creates an index fund that holds a basket of model tokens, weighted by market cap or accuracy:
from panda import contract, constructor, call, query, event
@contract
class ModelIndex:
"""Index fund holding a basket of model tokens."""
class State:
name: str = ""
models: dict = {} # address -> weight (0-100)
total_shares: int = 0
shares: dict = {} # holder -> share count
nav: float = 0.0 # net asset value
@constructor
def deploy(self, ctx, name: str, models: dict):
self.state.name = name
self.state.models = models # {"0xModel1": 40, "0xModel2": 30, "0xModel3": 30}
@call
def buy_shares(self, ctx, amount: int):
"""Buy index shares. Payment is split across underlying models by weight."""
self.state.total_shares += amount
self.state.shares[ctx.sender] = self.state.shares.get(ctx.sender, 0) + amount
self.emit(event.SharesBought(buyer=ctx.sender, amount=amount))
@call
def sell_shares(self, ctx, amount: int):
"""Sell index shares back."""
bal = self.state.shares.get(ctx.sender, 0)
if bal < amount:
raise ValueError("Insufficient shares")
self.state.shares[ctx.sender] = bal - amount
self.state.total_shares -= amount
self.emit(event.SharesSold(seller=ctx.sender, amount=amount))
@query
def get_nav(self) -> float:
return self.state.nav
@query
def get_portfolio(self) -> dict:
return self.state.models
Full Code Examples
Deploy and Trade a Tokenized Model
from panda_sdk import PandaClient
client = PandaClient("http://localhost:9650")
# 1. Deploy the TokenizedModel
model_addr = client.deploy_contract(
from_address="0xCreator",
source=open("tokenized_model.py").read(),
constructor_args={"name": "FraudNet", "symbol": "FNET", "category": "ML"},
)
# 2. Train the model
client.call_contract("0xCreator", model_addr, "train", {
"features": [[100, 5000], [200, 8000], [150, 6000]],
"labels": [0, 1, 0],
})
# 3. Buy tokens via bonding curve
client.call_contract("0xBuyer", model_addr, "buy", {"amount": 1000}, value=5.0)
# 4. Run a paid prediction
client.call_contract("0xUser", model_addr, "predict", {
"features": [[250, 10000]],
}, value=0.01)
# 5. Claim dividends as a token holder
client.call_contract("0xBuyer", model_addr, "claim_dividends", {})
# 6. Sell tokens back
client.call_contract("0xBuyer", model_addr, "sell", {"amount": 500})