Model Marketplace
The Model Marketplace is a set of SDK types and system contracts that let you discover on-chain ML models, pay for inference, manage rent, and coordinate distributed inference across sharded replicas. It is composed of three pieces:
- Model SDK type -- A first-class Python object for interacting with any deployed model contract. Working end-to-end today:
Model(addr)handles dispatch inference, metadata, rent, and funding to the underlying contract via cross-contract calls. - RentCollector -- A system contract that manages model rent lifecycle (active, expired, evicted).
- StreamingInferenceCoordinator -- A system contract that routes multi-shard inference jobs and records attribution.
This guide covers all three.
What's live vs. coming: Direct
Model(addr)handles are fully functional ā every example below that constructs aModelfrom a known address works against thePaidModelandTokenizedModelcontracts shipped incontracts/marketplace/. Discovery (Model.find.all()/Model.find.by_kind()) returns an empty list until an on-chain model registry exists (see Model Discovery). Use direct address handles for now.
Model Type Overview
The Model type is imported from panda and wraps any on-chain model contract address. It provides a unified interface for inference, metadata, rent, and funding -- regardless of the underlying model's kind.
from panda import Model
m = Model("0xABC123...")
A Model instance exposes four surfaces:
| Surface | Access | Description |
|---|---|---|
| Top-level | m.infer(), m.predict(), m.kind | Inference and identity |
.meta | m.meta.accuracy, m.meta.version | Model metadata (read-only, cached) |
.rent | m.rent.pay(), m.rent.due | Rent status and payment |
.funds | m.funds.fund(), m.funds.balance | Inference credit balance |
Kind System
Every model has a kind that determines how inference is dispatched:
| Kind | Dispatch | Description |
|---|---|---|
"paid" | predict() | Model charges per-inference fee from caller's credit balance |
"tokenized" | inference() | Model requires a governance token to invoke |
"free" | predict() | Model is free to call (no credits required) |
"empty" | n/a | Null/zero model (no contract deployed) |
"unknown" | n/a | Contract exists but does not conform to a known model interface |
The m.kind property is resolved by querying the model contract's on-chain metadata. You do not set it manually.
Using Model in Contracts
The most common use case is calling one model from inside another contract. The Model type uses cross-contract calls under the hood, so inference is fully on-chain and gas-metered.
Basic Inference
from panda import contract, constructor, call, query, event, Model
@contract
class PriceForecast:
"""Calls an external ML model to forecast token prices."""
class State:
model_address: str = ""
owner: str = ""
last_prediction: float = 0.0
@constructor
def deploy(self, ctx, model_address: str):
self.state.model_address = model_address
self.state.owner = ctx.sender
@call
def forecast(self, ctx, features: list, payment: int = 0) -> float:
m = Model(self.state.model_address)
# infer() takes a dict whose keys are splatted as keyword arguments
# to the model contract's predict()/inference() method. PaidModel and
# TokenizedModel both expect `x` (the feature rows) and optional `payment`.
result = m.infer({"x": features, "payment": payment})
self.state.last_prediction = result
self.emit(event.Forecasted(
requester=ctx.sender,
prediction=result,
))
return result
@query
def get_prediction(self) -> float:
return self.state.last_prediction
@query
def model_info(self) -> dict:
m = Model(self.state.model_address)
return {
"address": m.address,
"kind": m.kind,
"accuracy": m.meta.accuracy,
"version": m.meta.version,
"ready": m.meta.is_ready,
}
Predict vs Infer
m.infer(inputs) and m.predict(inputs) are aliases -- they call the same underlying dispatch. Use whichever reads better in context:
# These are equivalent:
result = m.infer({"x": [[1.0, 2.0, 3.0]], "payment": 100})
result = m.predict({"x": [[1.0, 2.0, 3.0]], "payment": 100})
inputs is a dict whose keys are passed straight through as keyword arguments to the underlying contract method. The shipped model contracts (PaidModel, TokenizedModel) take x (the feature rows) and an optional integer payment, so a typical call is {"x": rows, "payment": amount}.
Under the hood, the Model type dispatches based on m.kind:
"paid"and"free"models route to the contract'spredict()method."tokenized"models route to the contract'sinference()method."empty"and"unknown"models raiseValueError.
Pre-flight Checks
Before calling inference, you may want to verify the model is ready and the caller has sufficient funds:
@call
def safe_forecast(self, ctx, features: list) -> float:
m = Model(self.state.model_address)
if not m.meta.is_ready:
raise ValueError("Model is not ready for inference")
if m.kind == "paid" and m.funds.balance <= 0:
raise ValueError("No inference credits -- call m.funds.fund() first")
if m.rent.due:
raise ValueError("Model rent is past due -- inference disabled")
return m.infer({"x": features, "payment": 100})
Model Sub-types
Metadata: m.meta
The .meta sub-type provides read-only access to the model's on-chain metadata. Metadata is cached after the first access and can be explicitly invalidated.
m = Model("0xABC...")
m.meta.accuracy # float, 0.0 - 1.0 (e.g. 0.94)
m.meta.version # int (e.g. 3)
m.meta.is_ready # bool -- True if the model has been trained and is serving
m.meta.raw() # dict -- full metadata as stored on-chain
m.meta.invalidate() # force re-fetch on next access
| Property | Type | Description |
|---|---|---|
accuracy | float | Model accuracy score, range 0.0 to 1.0 |
version | int | Model version number (increments on retrain) |
is_ready | bool | Whether the model is trained and accepting inference |
| Method | Returns | Description |
|---|---|---|
raw() | dict | Full metadata dict as stored in the model contract's state |
invalidate() | None | Clears the local cache so the next property access re-fetches from chain |
Caching behavior: The first access to any .meta property triggers a cross-contract query to the model. Subsequent accesses within the same transaction return the cached value. Call invalidate() if the model may have been retrained between accesses.
Rent: m.rent
The .rent sub-type lets you check and pay rent for a model. Rent is managed by the RentCollector system contract.
m = Model("0xABC...")
m.rent.due # bool -- True if rent is past due
m.rent.pay(1000) # buy 1000 blocks of rent (extends paid_through_block)
m.rent.status() # dict with full rent state
| Property | Type | Description |
|---|---|---|
due | bool | True if the model's rent has expired (past paid_through_block) |
| Method | Parameters | Returns | Description |
|---|---|---|---|
pay(amount) | amount: int | None | Buy amount blocks of rent. The model's paid_through_block advances by amount from whichever is later: its current paid-through block or the current height (so lapsed rent restarts the clock now). amount must be positive |
status() | (none) | dict | Rent status reported by the model contract's rent_status query (the SDK reads blocks_remaining from it) |
status() returns whatever the model contract's rent_status query reports. The SDK's due property reads the blocks_remaining field from that dict and treats the model as past due when blocks_remaining <= 0:
{
"paid_through_block": 95000, # block up to which rent is funded
"current_block": 93580, # the model's last-active block
"blocks_remaining": 1420, # blocks of rent still funded; <= 0 means past due
"status": "active", # "active" while blocks_remaining > 0, else "due"
}
(This is the exact dict the shipped PaidModel.rent_status query returns; TokenizedModel exposes the same metadata/funding surface but does not implement rent.)
Funding: m.funds
The .funds sub-type manages prepaid inference credits. The dispatch is kind-aware: for "paid" models it calls fund / reads get_balance; for "tokenized" models it calls buy (the bonding-curve purchase) / reads get_holdings. For "free" and "empty" models funding is unsupported (fund raises NotImplementedError, balance returns 0).
m = Model("0xABC...")
m.funds.balance # int -- current prepaid credit balance (paid) or token holdings (tokenized)
m.funds.fund(5000) # paid: add 5000 prepaid credits; tokenized: buy 5000 tokens
| Property | Type | Description |
|---|---|---|
balance | int | For "paid" models, the model's prepaid credit balance (get_balance().balance); for "tokenized" models, the caller's token holdings (get_holdings().balance). 0 for kinds without a funding surface |
| Method | Parameters | Returns | Description |
|---|---|---|---|
fund(amount) | amount: int (positive) | None | For "paid" models, add amount prepaid credits (fund); for "tokenized" models, buy amount tokens (buy). Raises NotImplementedError for kinds without funding |
Model Discovery
The Model class exposes class-level discovery methods via Model.find, plus a Model.empty() helper.
Status: Discovery is not live yet.
Model.find.all()andModel.find.by_kind()resolve their results through an on-chain system-contract registry (ModelExchange/PaidModelRegistry). That registry does not exist on-chain today āget_system_contractreturnsNoneā so both methods return an empty list and degrade gracefully rather than erroring. Until the registry lands, constructModelhandles directly from a known address, which is the fully working path:m = Model("0xABC123...") # works today
from panda import Model
# Discovery (returns [] today -- no on-chain registry yet):
all_models = Model.find.all()
paid_models = Model.find.by_kind("paid")
# Working today -- a direct handle to a known model address:
m = Model("0xABC123...")
# Create a null/zero model (useful as a default):
null_model = Model.empty()
| Method | Parameters | Returns | Description |
|---|---|---|---|
Model.find.all() | (none) | list[Model] | Every model the chain knows about. Returns [] until an on-chain model registry exists |
Model.find.by_kind(k) | k: str | list[Model] | Models filtered by kind ("paid", "free", "tokenized"). Returns [] until an on-chain model registry exists |
Model.empty() | (none) | Model | A null model with kind "empty" (works today) |
Discovery in a Contract
This pattern shows how discovery is intended to be used once the registry lands. Today Model.find.by_kind("paid") returns [], so select_best would raise "No ready paid models found" ā pass an explicit address (or maintain your own allowlist of model addresses) until discovery is live.
from panda import contract, call, query, Model
@contract
class ModelAggregator:
"""Finds the best model on-chain and uses it."""
class State:
selected_model: str = ""
@call
def select_best(self, ctx):
"""Pick the model with the highest accuracy."""
models = Model.find.by_kind("paid")
best = None
best_acc = 0.0
for m in models:
if m.meta.is_ready and m.meta.accuracy > best_acc:
best = m
best_acc = m.meta.accuracy
if best is None:
raise ValueError("No ready paid models found")
self.state.selected_model = best.address
@query
def get_selected(self) -> dict:
if not self.state.selected_model:
return {"selected": False}
m = Model(self.state.selected_model)
return {
"selected": True,
"address": m.address,
"kind": m.kind,
"accuracy": m.meta.accuracy,
}
Rent Economics: RentCollector
The RentCollector is a system contract that enforces rent on deployed models. Models that do not pay rent are first marked expired, then evicted after a grace period. Anyone can trigger expiration and eviction -- the eviction caller receives a small bounty as incentive.
Lifecycle
active āā[block > paid_through]āā> expired
^ |
| pay_rent | [block > expired + grace]
| v
active <āā reinstate <āā evicted
A model starts in the active state when rent is paid. When the current block exceeds the paid_through_block, anyone can call mark_expired to transition it to expired. If rent remains unpaid past the grace window, anyone can call evict to move it to evicted and claim a bounty. The owner can reinstate an evicted model by paying all back rent.
Default Parameters
These are constructor parameters of the RentCollector contract:
| Parameter | Default | Description |
|---|---|---|
sweeper_bounty_bps | 50 (0.5%) | Percentage of a model's residual balance paid to the account that triggers evict. Must be 0..1000 (0..10%) |
rent_grace_blocks | 7200 blocks (~6 hours) | Window after expiration before eviction is allowed |
max_per_call | 100 | Rate-limit on how many models one sweep transaction may touch |
Per-block rent itself is tracked on each model contract (via its own rent_paid_through_block metadata), not on the RentCollector.
Methods
| Method | Type | Parameters | Description |
|---|---|---|---|
mark_expired | @call | model_addr: str | Transition from active to expired (anyone can call when overdue) |
evict | @call | model_addr: str | Evict after grace period; caller receives the sweeper_bounty_bps bounty |
reinstate | @call | model_addr: str | Owner pays back rent (extends the model's own rent_paid_through_block) to restore an evicted model |
register_listing | @call | model_addr: str, registry_addr: str, listing_id: int | Record where a model is listed so evict can un-list it |
get_status | @query | model_addr: str | Return { "status", "expired_at_block" } |
stats | @query | (none) | Return aggregate sweep statistics |
list_expired | @query | limit: int = 100 | Return up to limit model addresses currently expired (ready to evict) |
Paying Rent
From a contract that manages its own model:
from panda import contract, constructor, call, query, event, Model
@contract
class ManagedModel:
"""A model contract that pays its own rent."""
class State:
model: dict = {}
owner: str = ""
self_address: str = ""
@constructor
def deploy(self, ctx):
self.state.owner = ctx.sender
# Stash our own address so @query methods (which receive no ctx) can
# build a Model handle to this contract.
self.state.self_address = ctx.contract_address
@call
def top_up_rent(self, ctx, blocks: int):
"""Owner tops up rent for the specified number of blocks."""
if ctx.sender != self.state.owner:
raise ValueError("Only owner can pay rent")
m = Model(ctx.contract_address)
m.rent.pay(blocks) # amount is the number of blocks of rent to buy
self.emit(event.RentPaid(
blocks=blocks,
paid_by=ctx.sender,
))
@query
def rent_status(self) -> dict:
m = Model(self.state.self_address)
return m.rent.status()
Eviction Bounty
When a model is evicted, the caller who triggers evict receives 0.5% of the model's accumulated rent balance as a bounty. This incentivizes network participants to clean up expired models.
# Anyone can call this -- no special permissions needed
from panda import call_contract
# Mark expired first (required before eviction)
call_contract("0xRentCollector", "mark_expired", model_addr="0xDeadModel")
# Wait for grace period (rent_grace_blocks, default 7200)...
# Evict and claim bounty
call_contract("0xRentCollector", "evict", model_addr="0xDeadModel")
# Caller receives the sweeper_bounty_bps share (default 0.5%) of the model's residual balance
Sweeping the Registry
Keeper bots can discover models that are ready to evict with the list_expired query, then call evict on each. Use get_status to check an individual model.
from panda import call_contract, query_contract
# Discover models already in the expired state (up to `limit`)
expired = query_contract("0xRentCollector", "list_expired", limit=100)
for addr in expired:
# Each evict is rate-limited by the contract's max_per_call guard
call_contract("0xRentCollector", "evict", model_addr=addr)
Distributed Inference: StreamingInferenceCoordinator
The StreamingInferenceCoordinator is a system contract that manages multi-shard inference jobs. Large models that are split across multiple replicas use this coordinator to route inference through each layer, track attribution, and emit a final receipt.
How It Works
- A caller requests inference, specifying the model, an inputs hash, and the number of shards.
- The coordinator creates a job and assigns it a unique
job_id. - Each shard processes its layer and calls
advance_layerwith the result hash and attribution data. - When all layers are complete, the caller (or the final shard) calls
completeto finalize the job. - The coordinator emits an
InferenceReceiptevent with the full route and attribution chain.
Methods
| Method | Type | Parameters | Returns | Description |
|---|---|---|---|---|
request_inference | @call | model_address: str, inputs_hash: str, num_shards: int | int (job_id) | Create a new inference job |
advance_layer | @call | job_id: int, shard_id: int, replica_address: str, outputs_hash: str, attribution_contributor: str = "", attribution_weight_bps: int = 0 | None | Record a shard's contribution to the job (requester or owner only) |
complete | @call | job_id: int, final_output_hash: str | dict (the receipt) | Finalize the job and emit receipt (requester or owner only) |
register_replica_set | @call | shard_id: int, replica_set_address: str | None | Bind a replica set to a shard (owner only) |
get_job | @query | job_id: int | dict | Return current job state |
get_receipt | @query | job_id: int | dict | Return the finalized receipt (after complete) |
Requesting Inference
from panda import contract, call, query, event, call_contract
@contract
class InferenceClient:
"""Requests distributed inference and tracks job status."""
class State:
coordinator: str = ""
pending_jobs: dict = {}
owner: str = ""
@call
def request(self, ctx, model_address: str, inputs_hash: str, num_shards: int):
"""Submit an inference request to the coordinator."""
job_id = call_contract(
self.state.coordinator,
"request_inference",
model_address=model_address,
inputs_hash=inputs_hash,
num_shards=num_shards,
)
jobs = dict(self.state.pending_jobs)
jobs[job_id] = {
"model": model_address,
"status": "pending",
"shards_total": num_shards,
"shards_done": 0,
}
self.state.pending_jobs = jobs
self.emit(event.InferenceRequested(
job_id=job_id,
model=model_address,
num_shards=num_shards,
))
return job_id
@query
def job_status(self, job_id: str) -> dict:
job = call_contract(
self.state.coordinator,
"get_job",
job_id=job_id,
)
return job
Shard Processing
Each shard is a separate contract (or off-chain worker with on-chain settlement) that processes one layer of the model and reports back to the coordinator.
from panda import contract, call, event, call_contract
@contract
class ShardWorker:
"""Processes one layer of a sharded model."""
class State:
coordinator: str = ""
shard_id: int = 0
owner: str = ""
@call
def process_layer(self, ctx, job_id: int, input_hash: str):
"""Run inference on this shard's layer and report the result."""
# Perform actual computation (model-specific)
result_hash = self._compute(input_hash)
# Report result to coordinator. shard_id must equal the job's next
# expected layer (the coordinator enforces monotonic shard order).
call_contract(
self.state.coordinator,
"advance_layer",
job_id=job_id,
shard_id=self.state.shard_id,
replica_address=ctx.contract_address,
outputs_hash=result_hash,
attribution_contributor=ctx.contract_address,
attribution_weight_bps=10000,
)
self.emit(event.LayerProcessed(
job_id=job_id,
shard_id=self.state.shard_id,
result_hash=result_hash,
))
return result_hash
def _compute(self, input_hash: str) -> str:
# Model-specific inference logic
return "0xresult..."
Replica Sets
For high availability, each shard can have multiple replicas. Use register_replica_set to associate a shard ID with a replica set address (typically a contract that manages failover).
from panda import call_contract
# Register a replica set for shard 0 (shard IDs are integers)
call_contract(
"0xCoordinator",
"register_replica_set",
shard_id=0,
replica_set_address="0xReplicaSet0",
)
InferenceReceipt
When a distributed inference job completes, the coordinator emits an InferenceReceipt event. This event contains the full routing and attribution chain, providing an auditable record of which shards contributed to the final output.
Event Shape
event.InferenceReceipt(
job_id=42,
model_address="0xModelAddress",
inputs_hash="0xinputs...",
outputs_hash="0xoutput...",
# Each route entry is [shard_id, replica_address, outputs_hash]
route=[
[0, "0xReplica0", "0xhash0..."],
[1, "0xReplica1", "0xhash1..."],
[2, "0xReplica2", "0xhash2..."],
],
# Each attribution entry is [shard_id, contributor, weight_bps]
attributions=[
[0, "0xWorker0", 3300],
[1, "0xWorker1", 3300],
[2, "0xWorker2", 3400],
],
shard_count=3,
start_block=100,
end_block=102,
)
Fields
| Field | Type | Description |
|---|---|---|
job_id | int | Unique identifier for the inference job |
model_address | str | Address of the model contract |
inputs_hash | str | Hash of the original inputs |
outputs_hash | str | Hash of the final output |
route | list[list] | Ordered route entries, each [shard_id, replica_address, outputs_hash] |
attributions | list[list] | Attribution records, each [shard_id, contributor, weight_bps] (only present when attribution was supplied) |
shard_count | int | Number of shards (num_shards) for the job |
start_block | int | Block number when the job was created |
end_block | int | Block number when the job was finalized |
Querying Receipts
After a job completes, use get_receipt to retrieve the full receipt:
from panda import query_contract
receipt = query_contract("0xCoordinator", "get_receipt", job_id=42)
print(receipt["model_address"]) # "0xModelAddress"
print(receipt["route"]) # [[0, "0xReplica0", "0xhash0..."], ...]
print(receipt["outputs_hash"]) # "0xoutput..."
print(len(receipt["attributions"])) # number of attributed layers
Receipts are also indexed by the block explorer and visible on the model's detail page.
ModelRegistry
The ModelRegistry is the discovery layer for the marketplace -- the "app store" for on-chain models. Where Model.find gives you a direct handle to a known address, the registry is where creators publish listings so consumers can discover models they didn't already know about.
A creator registers their deployed PaidModel (or TokenizedModel) contract address along with a name, description, category, task type, a self-reported accuracy, and an informational price. Users then browse, filter, search, and rate listings, so a consumer can find a model by category or task type and rank candidates by community rating. Each listing points to a real deployed contract where inference actually happens; the registry only holds metadata and reverse indexes.
Registering a Model
from panda import call_contract
# A creator lists their deployed PaidModel contract
listing_id = call_contract(
"0xRegistry",
"register_model",
model_address="0xMyPaidModel",
model_name="Fraud Classifier v2",
description="Logistic regression fraud detector trained on 1M txns",
category="finance",
task_type="classification",
accuracy=0.94,
price_per_call=1000,
)
Duplicate addresses are rejected, so a given contract appears at most once in the registry.
Discovering Models
from panda import query_contract
# Browse a category
listings = query_contract("0xRegistry", "list_models", category="finance", active_only=True)
# Search by task type, capped at a price, sorted by rating (desc)
best = query_contract("0xRegistry", "search_models", task_type="classification", max_price=2000)
# Reverse lookup: which listing points at this contract?
listing = query_contract("0xRegistry", "get_listing_by_address", model_address="0xMyPaidModel")
Consumers can also rate_model(listing_id, score) (1--5, one rating per address) to build the running avg_rating that search_models sorts by.
State highlights
owner, next_listing_id, listings (listing_id -> metadata), categories (category -> listing ids), address_to_listing (reverse index), ratings (listing_id -> {rater -> score}), creator_listings (creator -> listing ids).
Methods
| Method | Kind | Description |
|---|---|---|
deploy() | constructor | Sets the registry owner. |
register_model(model_address, model_name, description, category, task_type, accuracy, price_per_call) | call | Registers a model contract; indexes it by category and creator. Rejects duplicate addresses. Returns the listing id. Emits ModelRegistered. |
deactivate_model(listing_id) | call | Creator or registry owner marks a listing inactive. Emits ModelDeactivated. |
rate_model(listing_id, score) | call | Rate a listing 1--5 (one rating per address); recomputes the running average. Emits ModelRated. |
update_accuracy(listing_id, accuracy) | call | Creator updates the listing's accuracy metric. Emits AccuracyUpdated. |
get_listing(listing_id) | query | Full listing metadata. |
list_models(category, active_only) | query | List listings, optionally filtered by category and active flag. |
list_by_creator(creator) | query | All listings registered by a given creator. |
search_models(task_type, max_price) | query | Filter active listings by task type and max price, sorted by avg_rating descending. |
get_listing_by_address(model_address) | query | Reverse lookup: find a listing by its model contract address. |
listing_count() | query | Total number of listings ever registered. |
ShardReplicaSet
When a shard is served by more than one worker, a ShardReplicaSet contract sits behind that shard and picks which replica handles the next layer. It is the health-aware routing layer that the register_replica_set binding above points to: one ShardReplicaSet per shard.
Replicas register themselves (owner-gated in v1). The set records each replica's success/fail counts and rolling latency, and exposes select_replica() to return the next healthy replica. Selection is round-robin -- the cursor is advanced by report_completion so select_replica stays a pure read -- and skips any replica below the configured success-rate floor (min_success_rate_bps) or above the latency ceiling (max_latency_ms). A reporter whitelist prevents an unrelated account from flooding fail-reports to knock a target replica out of rotation (selection DoS).
from panda import call_contract, query_contract
# Deployed with health thresholds for shard 0
# (min_success_rate_bps, max_latency_ms, owner set at deploy time)
# Owner registers replicas for this shard
call_contract("0xReplicaSet0", "register_replica", replica_address="0xReplicaA")
call_contract("0xReplicaSet0", "register_replica", replica_address="0xReplicaB")
# Pick the next healthy replica (round-robin, skips unhealthy)
replica = query_contract("0xReplicaSet0", "select_replica")
# A whitelisted reporter (or the replica itself) records how a layer went;
# this also advances the round-robin cursor
call_contract(
"0xReplicaSet0",
"report_completion",
replica_address=replica,
latency_ms=42,
success=True,
)
Bind the set to a shard on the coordinator with register_replica_set(shard_id, replica_set_address) (see Replica Sets above), then let the coordinator route layers through the healthy replica that select_replica() returns.
State highlights
owner, shard_id, replicas (address -> {success, fail, total_latency_ms, registered_at}), rr_cursor, min_success_rate_bps, max_latency_ms, reporters.
Methods
| Method | Kind | Description |
|---|---|---|
deploy(shard_id, min_success_rate_bps, max_latency_ms, owner) | constructor | Configures the shard's health thresholds. Emits ShardReplicaSetDeployed. |
register_replica(replica_address) | call | Owner registers a replica for this shard. Emits ReplicaRegistered. |
deregister_replica(replica_address) | call | Owner removes a replica. Emits ReplicaDeregistered. |
add_reporter(reporter_address) / remove_reporter(reporter_address) | call | Owner manages the whitelist allowed to call report_completion. |
report_completion(replica_address, latency_ms, success) | call | Owner, a whitelisted reporter, or the replica itself records a layer completion; advances the round-robin cursor. |
advance_cursor() | call | Moves the round-robin pointer forward without recording stats (no-report path). |
select_replica() | query | Returns the next healthy replica (round-robin, skipping unhealthy) or "" if none. |
list_replicas() | query | Per-replica success/fail, success-rate bps, avg latency, and health flag. |
stats() | query | Shard id, replica count, and configured thresholds. |
API Reference
Model
| Property / Method | Type | Returns | Description |
|---|---|---|---|
Model(address) | constructor | Model | Create a Model handle from an on-chain address |
.address | property | str | The on-chain contract address |
.kind | property | str | Model kind: "paid", "tokenized", "free", "empty", "unknown" |
.infer(inputs) | method | varies | Run inference (dispatches by kind) |
.predict(inputs) | method | varies | Alias for .infer() |
.meta | sub-type | ModelMeta | Metadata accessor |
.rent | sub-type | ModelRent | Rent accessor |
.funds | sub-type | ModelFunds | Funding accessor |
ModelMeta
| Property / Method | Type | Returns | Description |
|---|---|---|---|
.accuracy | property | float | Model accuracy (0.0 -- 1.0) |
.version | property | int | Model version number |
.is_ready | property | bool | Whether the model is trained and serving |
.raw() | method | dict | Full metadata dict from chain |
.invalidate() | method | None | Clear local metadata cache |
ModelRent
| Property / Method | Type | Returns | Description |
|---|---|---|---|
.due | property | bool | True if rent is past due |
.pay(amount) | method | None | Buy amount blocks of rent (extends paid_through_block) |
.status() | method | dict | Full rent status dict (paid_through_block, current_block, blocks_remaining, status) |
ModelFunds
| Property / Method | Type | Returns | Description |
|---|---|---|---|
.balance | property | int | Prepaid credits (paid) or token holdings (tokenized); 0 for kinds without funding |
.fund(amount) | method | None | Add credits (paid: fund) or buy tokens (tokenized: buy); raises NotImplementedError otherwise |
Model.find (Discovery)
| Method | Parameters | Returns | Description |
|---|---|---|---|
Model.find.all() | (none) | list[Model] | All registered models. Returns [] until an on-chain registry exists |
Model.find.by_kind(k) | k: str | list[Model] | Models filtered by kind. Returns [] until an on-chain registry exists |
Model.empty() | (none) | Model | Null model with kind "empty" (works today) |
RentCollector
| Method | Type | Parameters | Returns | Description |
|---|---|---|---|---|
pay_rent | @call | model_address: str, blocks: int | None | Pay rent for N blocks |
mark_expired | @call | model_address: str | None | Mark overdue model as expired |
evict | @call | model_address: str | None | Evict after grace; caller gets 0.5% bounty |
reinstate | @call | model_address: str | None | Owner restores evicted model |
batch_mark_expired | @call | addresses: list | None | Bulk expire overdue models |
get_status | @query | model_address: str | dict | Rent status for a model |
StreamingInferenceCoordinator
| Method | Type | Parameters | Returns | Description |
|---|---|---|---|---|
request_inference | @call | model_address: str, inputs_hash: str, num_shards: int | int | Create job, returns job_id |
advance_layer | @call | job_id: int, shard_id: int, replica_address: str, outputs_hash: str, attribution_contributor: str = "", attribution_weight_bps: int = 0 | None | Record shard result |
complete | @call | job_id: int, final_output_hash: str | dict | Finalize job, emit receipt, return receipt |
register_replica_set | @call | shard_id: int, replica_set_address: str | None | Associate shard with replicas (owner only) |
get_job | @query | job_id: int | dict | Current job state |
get_receipt | @query | job_id: int | dict | Finalized receipt |
Related Guides
- ML Contracts -- Training and serving models on-chain with
panda.ml - SDK Reference -- Full decorator and type reference
- DeFi Patterns -- Escrow, AMMs, and cross-contract composition
- Contract Development -- Writing, testing, and deploying contracts