Paid Inference

Introduction

The PaidModel pattern lets you deploy a trained machine learning model as a Panda smart contract and charge users for every prediction. Revenue accumulates on-chain and the model owner can withdraw earnings at any time. This is the simplest way to monetize ML on PandaChain.

How It Works

  1. Deploy a PaidModel contract with your model type and pricing
  2. Train the model on-chain with your dataset
  3. Users pay the per-call price to submit features and receive predictions
  4. Revenue accumulates in the contract balance
  5. Withdraw your earnings whenever you want

Creating a PaidModel Contract

Here is a complete PaidModel contract that trains a linear regression model and charges per prediction:

"""
PaidModel -- Deploy an ML model and charge per prediction.

Demonstrates:
- Training a model and persisting weights on-chain
- Charging a per-call fee for inference
- Tracking usage statistics (calls, revenue, unique users)
- Owner-only withdraw and price management
"""

from panda import contract, constructor, call, query, event
from panda.ml import LinearRegression, save_model, load_model, r2_score


@contract
class PaidModel:
    """ML model that charges per prediction."""

    class State:
        owner: str = ""
        name: str = ""
        description: str = ""
        category: str = "ML"
        model: dict = {}
        is_trained: bool = False
        accuracy: float = 0.0
        price_per_call: float = 0.01
        total_calls: int = 0
        total_revenue: float = 0.0
        unique_users: dict = {}
        balance: float = 0.0

    @constructor
    def deploy(self, ctx, name: str, description: str = "", category: str = "ML", price: float = 0.01):
        """Deploy the paid model contract.

        Args:
            name: Human-readable model name.
            description: What the model does.
            category: Model category (ML, NLP, Vision, Audio, RL).
            price: Price per prediction call in PANDA.
        """
        self.state.owner = ctx.sender
        self.state.name = name
        self.state.description = description
        self.state.category = category
        self.state.price_per_call = price
        self.emit(event.ModelDeployed(name=name, owner=ctx.sender, price=price))

    @call
    def train(self, ctx, features: list, labels: list):
        """Train or retrain the model. Owner only.

        Args:
            features: 2D feature matrix [[f1, f2, ...], ...]
            labels: Target values [y1, y2, ...]
        """
        if ctx.sender != self.state.owner:
            raise ValueError("Only the owner can train the model")
        if len(features) != len(labels):
            raise ValueError("features and labels must have same length")

        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(
            samples=len(features),
            accuracy=self.state.accuracy,
            trainer=ctx.sender,
        ))

    @call
    def predict(self, ctx, features: list) -> list:
        """Run inference. Caller must pay price_per_call.

        Args:
            features: Input feature vectors [[f1, f2, ...], ...]

        Returns:
            List of predictions.
        """
        if not self.state.is_trained:
            raise ValueError("Model not trained yet")

        # Charge the caller
        if ctx.value < self.state.price_per_call:
            raise ValueError(f"Insufficient payment: need {self.state.price_per_call}, got {ctx.value}")

        model = load_model(self.state.model)
        predictions = model.predict(features)

        # Update stats
        self.state.total_calls += 1
        self.state.total_revenue += ctx.value
        self.state.balance += ctx.value
        self.state.unique_users[ctx.sender] = True

        self.emit(event.Prediction(
            caller=ctx.sender,
            num_features=len(features),
            payment=ctx.value,
        ))

        return [round(p, 4) for p in predictions]

    @call
    def set_price(self, ctx, price: float):
        """Update the price per call. Owner only."""
        if ctx.sender != self.state.owner:
            raise ValueError("Only the owner can update the price")
        old_price = self.state.price_per_call
        self.state.price_per_call = price
        self.emit(event.PriceUpdated(old=old_price, new=price))

    @call
    def withdraw(self, ctx):
        """Withdraw accumulated earnings. Owner only."""
        if ctx.sender != self.state.owner:
            raise ValueError("Only the owner can withdraw")
        amount = self.state.balance
        if amount <= 0:
            raise ValueError("Nothing to withdraw")
        self.state.balance = 0
        ctx.transfer(ctx.sender, amount)
        self.emit(event.Withdrawal(amount=amount, to=ctx.sender))

    @query
    def get_stats(self) -> dict:
        """Return model statistics."""
        return {
            "name": self.state.name,
            "description": self.state.description,
            "creator": self.state.owner,
            "category": self.state.category,
            "accuracy": self.state.accuracy,
            "pricePerCall": self.state.price_per_call,
            "totalCalls": self.state.total_calls,
            "totalRevenue": self.state.total_revenue,
            "uniqueUsers": len(self.state.unique_users),
            "isTrained": self.state.is_trained,
            "withdrawableBalance": self.state.balance,
        }

How Pricing Works

The PaidModel uses a simple pay-per-call model:

  1. The owner sets a price_per_call at deploy time (or updates it later with set_price)
  2. Every call to predict() checks that ctx.value >= price_per_call
  3. The payment is added to the contract's internal balance
  4. The owner can call withdraw() at any time to claim accumulated earnings

Pricing is denominated in PANDA tokens. A typical range is:

Model TypeTypical PriceUse Case
Simple regression0.001 - 0.01Price predictions, simple forecasts
Classification0.005 - 0.05Fraud detection, spam filtering
NLP models0.01 - 0.1Sentiment analysis, text classification
Vision models0.02 - 0.2Image classification, object detection

Withdrawing Earnings

The owner can call withdraw() at any time. This transfers the entire accumulated balance to the owner's address. The balance resets to zero after withdrawal.

# From the SDK:
from panda_sdk import PandaClient

client = PandaClient("http://localhost:9650")
result = client.call_contract(
    from_address="0xYourAddress",
    contract_address="0xModelAddress",
    method="withdraw",
    args={},
)
print(f"Withdrawn: {result}")

Using from the SDK

Deploy a PaidModel

from panda_sdk import PandaClient

client = PandaClient("http://localhost:9650")

# Deploy with constructor args
address = client.deploy_contract(
    from_address="0xYourAddress",
    source=open("paid_model.py").read(),
    constructor_args={
        "name": "FraudDetector",
        "description": "Detects fraudulent transactions",
        "category": "ML",
        "price": 0.01,
    },
)
print(f"Deployed at: {address}")

Train the Model

# Training data
features = [[100, 5000], [200, 8000], [150, 6000], [300, 12000]]
labels = [0, 1, 0, 1]  # 0 = legit, 1 = fraud

client.call_contract(
    from_address="0xYourAddress",
    contract_address=address,
    method="train",
    args={"features": features, "labels": labels},
)

Run a Prediction

result = client.call_contract(
    from_address="0xCallerAddress",
    contract_address=address,
    method="predict",
    args={"features": [[250, 10000]]},
    value=0.01,  # Pay the per-call price
)
print(f"Prediction: {result}")

Check Stats

stats = client.query_contract(
    contract_address=address,
    method="get_stats",
    args={},
)
print(f"Total calls: {stats['totalCalls']}")
print(f"Revenue: {stats['totalRevenue']}")
print(f"Accuracy: {stats['accuracy']}%")