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:

  1. An ML model -- trained on-chain using panda.ml, stored in contract state
  2. A PRC-20 token -- minted via a bonding curve, representing ownership shares
  3. 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:

  1. The fee is divided by total_supply to get the per-token dividend increment
  2. dividend_per_token is a cumulative counter that only increases
  3. When a holder calls claim_dividends(), they receive:
    owed = balance * (current_dividend_per_token - last_claimed_per_token)
    
  4. 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})