SDK Reference
Complete reference for the Panda SDK -- all decorators, classes, types, and testing utilities available for writing and testing Python smart contracts.
Installation
pip install panda-sdk
Quick Start
<!-- Source: contracts/examples/counter.py -->from panda import contract, constructor, call, query, event
@contract
class Counter:
class State:
count: int = 0
owner: str = ""
last_caller: str = ""
@constructor
def deploy(self, ctx, initial_count: int = 0):
self.state.owner = ctx.sender
self.state.count = initial_count
@call
def increment(self, ctx):
self.state.count = self.state.count + 1
self.state.last_caller = ctx.sender
self.emit(event.CountChanged(
new_count=self.state.count,
changed_by=ctx.sender,
))
@query
def get_count(self) -> int:
return self.state.count
Test locally:
from panda.testing import ContractTestRunner
runner = ContractTestRunner()
addr = runner.deploy("examples/counter.py", sender="alice")
runner.call(addr, "increment", sender="alice")
runner.call(addr, "increment", sender="alice")
result = runner.query(addr, "get_count")
assert result.return_value == 2
Deploy via the Contract Playground or programmatically via the SDK client.
Decorators
@contract
Marks a class as a Panda smart contract. The class must define an inner State class with typed fields and default values.
from panda import contract
@contract
class MyContract:
class State:
count: int = 0
name: str = ""
scores: list = []
metadata: dict = {}
active: bool = True
Requirements:
- Must have an inner
Stateclass - All state fields must have type annotations and default values
- Only one
@contractclass per file - Supported state types:
int,float,str,bool,bytes,list,dict
@constructor
Marks a method as the deploy-time constructor. Runs once when the contract is deployed. Cannot be called again after deployment.
from panda import constructor
@constructor
def deploy(self, ctx, name: str, symbol: str, initial_supply: int = 0):
self.state.name = name
self.state.symbol = symbol
self.state.owner = ctx.sender
if initial_supply > 0:
self.state.total_supply = initial_supply
self.state.balances[ctx.sender] = initial_supply
Notes:
- Receives
ctxas the first parameter (afterself) - Can accept deployment arguments
- Implicitly a
@callmethod (modifies state)
@call
Marks a method as state-mutating. Requires a transaction and costs gas.
from panda import call
@call
def transfer(self, ctx, to: str, amount: int):
sender_balance = self.state.balances.get(ctx.sender, 0)
if sender_balance < amount:
raise ValueError("Insufficient balance")
self.state.balances[ctx.sender] -= amount
self.state.balances[to] = self.state.balances.get(to, 0) + amount
Notes:
- First parameter after
selfis alwaysctx(execution context) - Can modify
self.state - Can emit events with
self.emit() - Can be
async deffor await-based patterns
@query
Marks a method as read-only. Free to call, no transaction needed.
from panda import query
@query
def balance_of(self, address: str) -> int:
return self.state.balances.get(address, 0)
Notes:
- Does NOT receive a
ctxparameter - Cannot modify state (mutations are discarded)
- Return value is sent back to the caller
@event
Events are not a method decorator. Instead, use event.Name(**fields) with self.emit():
from panda import event
# Inside a @call method:
self.emit(event.Transfer(sender=ctx.sender, recipient=to, amount=100))
self.emit(event.ModelTrained(samples=len(data), accuracy=0.95))
Event names are created dynamically -- no pre-definition needed. Events appear in transaction receipts and are indexed by the block explorer.
@private
Marks a contract or method for encrypted state and privacy-preserving execution.
from panda import private
# Apply to entire contract:
@private
@contract
class SecretModel:
class State:
weights: list = []
# Or apply to individual methods:
@call
@private
def update_secret(self, ctx, data: str):
self.state.secret_data = data
@proof
Requires ZK proof generation for a method.
from panda import proof
@call
@proof(type="validity", backend="auto")
def train(self, ctx, features: list, labels: list):
# Execution is proven correct via ZK proof
pass
@call
@proof(type="fraud")
def optimistic_op(self, ctx, data: list):
# Executes optimistically; can be challenged with a fraud proof
pass
Parameters:
| Parameter | Values | Default | Description |
|---|---|---|---|
type | "validity", "fraud" | "validity" | Proof type |
backend | "auto", "risc_zero", "halo2", "sp1" | "auto" | ZK backend |
See the Proofs guide for details.
@receiver
Marks a method as a cross-chain message handler. Can only be called with cross-chain context.
from panda import receiver
@receiver(chains=["ethereum", "solana"])
def on_deposit(self, ctx, amount: int, depositor: str):
ctx.source_chain # "ethereum" or "solana"
ctx.message_id # unique message ID
ctx.is_cross_chain # always True
self.state.total += amount
@receiver() # accepts from any chain
def on_any_message(self, ctx, data: str):
pass
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
chains | list[str] or None | None | Whitelist of allowed source chains. None = accept all. |
See the Cross-Chain guide for details.
@callback
Marks a method as a timer callback. Called by the timer system when a scheduled timer fires.
from panda import callback
@callback
def execute_withdrawal(self, ctx, to: str = "", amount: int = 0):
"""Called by the timer system -- cannot be called by users."""
self.state.balances[to] = self.state.balances.get(to, 0) + amount
Notes:
- Implicitly a
@callmethod (can modify state) - Cannot be called directly by users -- only the timer system can invoke it
@view
Marks a method as a visualization endpoint. Returns renderable content (charts, SVG, HTML).
from panda import view
@query
@view
def chart(self) -> str:
"""Auto-detect content type."""
import json
return json.dumps({"$schema": "https://vega.github.io/schema/vega-lite/v5.json", ...})
@query
@view(content_type="svg")
def diagram(self) -> str:
"""Explicit SVG content type."""
return '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
Supported content types: "svg", "vega", "html", "json", "png"
See the Visualizations guide for details.
@agent
Marks a contract as a PRC-Agent.
from panda import agent
@agent(capabilities=["inference", "cross_contract"])
@contract
class MyAgent:
class State:
active: bool = True
Valid capabilities: "inference", "training", "data_access", "cross_contract"
See the AI Agents guide for details.
Context Object
The ctx parameter passed to @call, @constructor, @callback, and @receiver methods:
@call
def my_method(self, ctx):
ctx.sender # Address of the transaction sender (str)
ctx.block_height # Current block number (int)
ctx.block_time # Block timestamp in seconds since epoch (int)
ctx.contract_address # This contract's address (str)
ctx.gas_remaining # Remaining gas budget in PCU (int)
ctx.chain_id # Chain identifier, e.g. "panda-eth-mainnet" (str)
ctx.gas_deposit # Gas deposit for async operations (int)
ctx.gas_deposit_remaining # Remaining gas deposit (int)
# Cross-chain fields (set on @receiver methods):
ctx.source_chain # Origin chain: "ethereum", "solana", etc. (str)
ctx.message_id # Unique cross-chain message ID (str)
ctx.is_cross_chain # True if this is a cross-chain call (bool)
Important: Use ctx.block_time instead of time.time() or datetime.now(). Standard library time functions are blocked for determinism.
State Management
Defining State
@contract
class MyContract:
class State:
count: int = 0
name: str = ""
scores: list = []
metadata: dict = {}
active: bool = True
model_bytes: bytes = b""
Reading and Writing
@call
def update(self, ctx, new_name: str):
current = self.state.name # read
self.state.name = new_name # write
self.state.metadata["updated"] = ctx.block_time # nested write
self.state.scores.append(42.0) # list append
Emitting Events
@call
def transfer(self, ctx, to: str, amount: int):
# ... logic ...
self.emit(event.Transfer(sender=ctx.sender, recipient=to, amount=amount))
Cross-Chain Messaging
Chain Class
from panda import Chain
eth = Chain.ethereum() # or Chain("ethereum")
sol = Chain.solana() # or Chain("solana")
l2 = Chain.l2() # or Chain("panda_l2")
hub = Chain.hub() # or Chain("panda_hub")
Methods:
| Method | Returns | Description |
|---|---|---|
chain.call(addr, method, **kwargs) | _CrossChainCallResult | Call a remote contract method |
chain.transfer(addr, amount, **kwargs) | _CrossChainCallResult | Transfer value |
chain.send(addr, **payload) | str (msg_id) | Send raw message |
chain.try_send(addr, **payload) | (bool, str) | Error-safe send |
chain.contract(addr) | RemoteContract | Get untyped remote handle |
chain.prc20(addr) | RemotePRC20 | Get typed PRC-20 handle |
Fire-and-Forget vs Await
# Fire-and-forget: msg_id is a string, execution continues
msg_id = eth.call("0xAddr", "mint", amount=100)
# Await: suspends until response arrives (method must be async def)
result = await eth.call("0xAddr", "mint", amount=100)
send_message / try_send_message
from panda import send_message, try_send_message
# Raises on error
msg_id = send_message("ethereum", "0xRecipient", action="mint", amount=100)
# Returns (success, msg_id_or_error) -- never raises
success, result = try_send_message("ethereum", "0xRecipient", amount=100)
RemoteContract
Untyped proxy for calling any method on a remote contract:
remote = eth.contract("0x1234...")
msg_id = remote.deposit(amount=100) # fire-and-forget
result = await remote.deposit(amount=100) # await
RemotePRC20
Typed proxy with PRC-20 methods:
usdc = eth.prc20("0xUSDC")
usdc.transfer(to="0xBob", value=1000)
usdc.approve(spender="0xDex", value=5000)
usdc.transfer_from(owner="0xAlice", to="0xBob", value=100)
usdc.mint(to="0xBob", amount=500)
usdc.balance_of(owner="0xBob")
usdc.total_supply()
See the Cross-Chain guide for full details and examples.
Cross-Contract Calls (Same Chain)
call_contract / query_contract
from panda import call_contract, query_contract
@call
def delegate(self, ctx, target: str, value: int):
result = call_contract(target, "process", value=value)
self.state.last_result = result
@query
def check_balance(self, token: str, owner: str) -> int:
return query_contract(token, "balance_of", owner=owner)
Contract Handle
Pythonic proxy for same-chain contract calls:
from panda import Contract
token = Contract("0x1234...")
token.transfer(to="0x5678", value=100) # calls call_contract
balance = token.balance_of(owner="0x5678") # calls call_contract
PRC20 Handle
Typed same-chain PRC-20 proxy with proper call/query routing:
from panda import PRC20
token = PRC20("0x1234...")
token.transfer(to="0x5678", value=100) # call_contract (state-mutating)
token.approve(spender="0xDex", value=5000) # call_contract
balance = token.balance_of(owner="0x5678") # query_contract (read-only)
supply = token.total_supply() # query_contract
name = token.name() # query_contract
symbol = token.symbol() # query_contract
Token Standard (FungibleToken)
The FungibleToken class provides an ERC-20-like implementation for building PRC-20 tokens. Here is the real PRC20Token reference implementation:
from panda import contract, constructor, call, query, event
from panda.token import FungibleToken, TokenError
@contract
class PRC20Token:
class State:
token: dict = {}
mint_authority: str = ""
freeze_authority: str = ""
frozen: dict = {}
def _t(self) -> FungibleToken:
return FungibleToken(self.state.token)
@constructor
def deploy(self, ctx, name: str, symbol: str, decimals: int = 18,
max_supply: int = 0, initial_supply: int = 0):
t = self._t()
t.configure(name=name, symbol=symbol, decimals=decimals, max_supply=max_supply)
self.state.mint_authority = ctx.sender
self.state.freeze_authority = ctx.sender
if initial_supply > 0:
t.mint(ctx.sender, initial_supply)
self.state.token = t.to_dict()
@call
def transfer(self, ctx, to: str, value: int):
self._not_frozen(ctx.sender)
self._not_frozen(to)
t = self._t()
t.transfer(ctx.sender, to, value)
self.state.token = t.to_dict()
self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=value))
@query
def balance_of(self, owner: str) -> int:
return self._t().balance_of(owner)
FungibleToken methods:
| Method | Description |
|---|---|
configure(name, symbol, decimals=18, max_supply=0) | Set token metadata |
mint(to, amount) | Create tokens |
burn(addr, amount) | Destroy tokens |
transfer(sender, to, amount) | Transfer tokens |
approve(owner, spender, amount) | Set spending allowance |
transfer_from(spender, owner, to, amount) | Transfer using allowance |
balance_of(addr) | Get balance |
allowance(owner, spender) | Get allowance |
total_supply() | Get total supply |
add_minter(addr) / remove_minter(addr) | Manage minting permissions |
to_dict() | Serialize for state storage |
Storage Data Structures
The panda.storage module provides deterministic data structures with guaranteed iteration order across all validators.
PandaMap
Ordered key-value store with prefix scanning:
from panda.storage import PandaMap
m = PandaMap()
m.set("alice", 100)
m.set("bob", 200)
m.get("alice") # 100
m.get("carol", 0) # 0 (default)
m.contains("bob") # True
m.delete("bob") # True
list(m.keys()) # ["alice"] (sorted)
m.prefix("al") # [("alice", 100)]
m.range("a", "c") # [("alice", 100)]
m.size() # 1
m.to_dict() # serialize for state storage
PandaSet
Ordered set with membership operations:
from panda.storage import PandaSet
s = PandaSet()
s.add("alice")
s.add("bob")
"alice" in s # True
s.members() # ["alice", "bob"] (sorted)
s.remove("bob") # True
s.membership_hash() # SHA-256 of sorted members
s.union(other_set) # new PandaSet
s.intersection(other) # new PandaSet
s.difference(other) # new PandaSet
s.to_dict() # serialize for state storage
PandaCounter
Named counters with overflow protection:
from panda.storage import PandaCounter
c = PandaCounter()
c.increment("views") # 1
c.increment("views", 10) # 11
c.decrement("views", 5) # 6
c.get("views") # 6
c.reset("views") # sets to 0
c.all() # {"views": 0} (sorted)
c.to_dict() # serialize
PandaSortedMap
Map sorted by numeric value with rank queries (leaderboards, priority queues):
from panda.storage import PandaSortedMap
lb = PandaSortedMap()
lb.set("alice", 100)
lb.set("bob", 200)
lb.set("carol", 150)
lb.top(2) # [("bob", 200), ("carol", 150)]
lb.bottom(2) # [("alice", 100), ("carol", 150)]
lb.rank("alice") # 2 (0-indexed from top)
lb.range_by_value(100, 160) # [("carol", 150), ("alice", 100)]
lb.to_dict() # serialize
PandaQueue
Bounded FIFO queue:
from panda.storage import PandaQueue
q = PandaQueue(max_size=100)
q.push({"event": "transfer", "amount": 50})
q.push({"event": "mint", "amount": 100})
q.peek() # {"event": "transfer", "amount": 50}
item = q.pop() # {"event": "transfer", "amount": 50}
q.size() # 1
q.is_empty() # False
q.is_full() # False
q.to_dict() # serialize
PandaTimeSeries
Time-indexed data with windowed aggregation:
from panda.storage import PandaTimeSeries
ts = PandaTimeSeries(max_points=10000)
ts.append(100, 42.0) # block_height=100, value=42.0
ts.append(101, 43.5)
ts.append(102, 41.0)
ts.window(100, 103) # [[100, 42.0], [101, 43.5], [102, 41.0]]
ts.window_mean(100, 103) # 42.166...
ts.window_sum(100, 103) # 126.5
ts.window_min(100, 103) # 41.0
ts.window_max(100, 103) # 43.5
ts.latest(2) # [[101, 43.5], [102, 41.0]]
ts.to_dict() # serialize
Async Contracts
sleep
Suspend execution for a number of blocks:
from panda import sleep
@call
async def delayed_withdrawal(self, ctx, amount: int):
self.state.pending[ctx.sender] = amount
await sleep(blocks=100)
# This runs 100 blocks later
self.state.pending.pop(ctx.sender)
self.state.balances[ctx.sender] += amount
@call
async def scheduled_at(self, ctx, target_block: int):
await sleep(until_block=target_block)
# Runs at the specified block height
delay (convenience)
Every contract automatically gets a delay method:
@call
async def step_one(self, ctx):
self.state.phase = 1
await self.delay(ctx, blocks=10)
self.state.phase = 2
Testing
ContractTestRunner
The ContractTestRunner simulates a Panda blockchain environment locally.
from panda.testing import ContractTestRunner
runner = ContractTestRunner(
chain_id="panda-test",
initial_block_height=1,
initial_block_time=1700000000,
)
Deploying Contracts
# From a file path
addr = runner.deploy("contracts/my_contract.py", sender="alice")
# From a @contract class
addr = runner.deploy(MyContract, sender="alice")
# With constructor arguments
addr = runner.deploy(MyToken, sender="alice", name="Test", symbol="TST", initial_supply=1000)
# With explicit address
addr = runner.deploy(MyContract, sender="alice", address="0xCustomAddr")
Calling Methods
# @call method
result = runner.call(addr, "transfer", sender="alice", to="bob", amount=100)
result.return_value # method return value
result.state # full state dict after execution
result.state_diff # changed fields: {field: {"old": v, "new": v}}
result.events # list of emitted events
result.logs # list of print() output
result.gas_used # gas consumed
# @query method
result = runner.query(addr, "balance_of", address="alice")
result.return_value # the returned value
State Inspection
state = runner.get_state(addr) # dict of state fields
info = runner.get_info(addr) # ContractInfo with address, owner, code_hash, state
Block Control
runner.set_block(height=100, time=1700001000)
runner.set_block_time(1700002000)
# block auto-advances by 1 after each call()
Cross-Contract Calls
The test runner automatically handles call_contract() and query_contract() between contracts deployed in the same runner:
token_addr = runner.deploy(TokenContract, sender="alice")
swap_addr = runner.deploy(SwapContract, sender="alice")
# SwapContract can call_contract(token_addr, "transfer", ...)
# and it will route through the test runner
Cross-Chain Testing
# Call a @receiver with cross-chain context
result = runner.call_cross_chain(
addr, "on_deposit",
sender="relayer",
source_chain="ethereum",
message_id="0xabc",
amount=1000,
depositor="0xAlice",
)
# Deliver response to a suspended await chain.call()
pending = runner.get_pending_cross_chain_calls()
msg_id = pending[0]["msg_id"]
result = runner.deliver_cross_chain_response(msg_id, response={"success": True})
# Inspect emitted cross-chain messages
messages = runner.get_cross_chain_messages()
runner.clear_cross_chain_messages()
Timer / Async Testing
# Fire all pending timers (from await sleep())
results = runner.fire_pending_timers(addr, sender="timer_system")
# Inspect hibernation snapshots (suspended coroutine state)
snapshots = runner.get_hibernation_snapshot(addr)
CallResult
Returned by runner.call() and runner.query():
@dataclass
class CallResult:
return_value: Any = None # Method return value
state: dict = {} # Full state after execution
state_diff: dict = {} # Changed fields
gas_used: int = 0 # Gas consumed
events: list = [] # Emitted events
logs: list = [] # print() output
stdout: str = "" # Captured stdout
gas_deposit_remaining: int = 0 # Remaining gas deposit
gas_deposit_refund: int = 0 # Gas deposit refund
Event Format
result.events[0]["event"] # "Transfer"
result.events[0]["data"]["amount"] # 100
result.events[0]["data"]["sender"] # "alice"
Types
Context
from panda import Context
@dataclass
class Context:
sender: str = ""
block_height: int = 0
block_time: int = 0
contract_address: str = ""
gas_remaining: int = 1_000_000
chain_id: str = "panda-local"
gas_deposit: int = 0
gas_deposit_remaining: int = 0
source_chain: str = "" # cross-chain only
message_id: str = "" # cross-chain only
is_cross_chain: bool = False # cross-chain only
ContractInfo
from panda import ContractInfo
@dataclass
class ContractInfo:
address: str = ""
owner: str = ""
code_hash: str = ""
state: dict = {}
Exceptions
| Exception | Description |
|---|---|
PandaError | Base exception for all SDK errors |
ContractError | Error during contract execution |
ValidationError | Contract source validation failure |
StateError | Invalid state operation |
GasExhaustedError | Gas limit exceeded |
Constants
from panda import ETHEREUM, SOLANA, PANDA_L2, PANDA_HUB
Complete Import Reference
# Core decorators
from panda import contract, constructor, call, query, callback, private, proof, view, agent, event, receiver
# Cross-contract calls (same chain)
from panda import call_contract, query_contract, Contract
# Typed token handle (same chain)
from panda import PRC20
# Cross-chain messaging
from panda import send_message, try_send_message, Chain, RemoteContract, RemotePRC20
from panda import ETHEREUM, SOLANA, PANDA_L2, PANDA_HUB
# Async
from panda import sleep
# Types
from panda import Context, CallResult, EventData, ContractInfo
from panda import PandaError, ContractError, ValidationError, StateError, GasExhaustedError
# State proxy
from panda import StateProxy
# Token implementation
from panda.token import FungibleToken
# Storage data structures
from panda.storage import PandaMap, PandaSet, PandaCounter, PandaSortedMap, PandaQueue, PandaTimeSeries
# Agent helpers
from panda import Agent, AgentError
# Testing
from panda.testing import ContractTestRunner
Example Contract Library
Panda ships with a comprehensive set of example contracts:
Getting Started
| Contract | Description |
|---|---|
hello_world.py | Simplest contract: greeting message with events |
counter.py | Counter with increment, decrement, add, reset, owner controls |
price_predictor.py | ML price prediction with LinearRegression |
fraud_detector.py | ML fraud detection with LogisticRegression |
prediction_market.py | Binary prediction market with betting and payouts |
Tokens and DeFi
| Contract | Description |
|---|---|
prc20_token.py | PRC-20 fungible token (reference implementation) |
prc721_collection.py | PRC-721 NFT collection |
token_swap.py | AMM-style token swap using cross-contract PRC-20 calls |
nft_marketplace.py | NFT marketplace with listing, buying, and royalties |
Cross-Chain
| Contract | Description |
|---|---|
cross_chain_bridge_v2.py | Full bridge with @receiver, await, fire-and-forget |
cross_chain_defi.py | DeFi swap with typed remote handles and multi-chain awaits |
cross_chain_messenger.py | Simple messaging between chains |
token_bridge.py | Token bridge with lock/mint/release flow |
Machine Learning
| Contract | Description |
|---|---|
regression_trainer.py | Linear regression on-chain |
logistic_classifier.py | Logistic regression classifier |
clustering_contract.py | K-Means clustering |
decision_tree_contract.py | Decision tree classifier |
neural_network_contract.py | MLP neural network |
ensemble_classifier.py | Random forest ensemble |
online_learner.py | SGD online learning |
image_classifier.py | Image classification pipeline |
model_marketplace.py | Buy/sell ML models on-chain |
torch_sentiment.py | PyTorch sentiment analysis |
tf_regression.py | TensorFlow regression |
numpy_data_analysis.py | NumPy-based data analysis |
numpy_matrix_ops.py | NumPy matrix operations |
numpy_signal_processor.py | Signal processing with NumPy |
pandas_data_pipeline.py | Pandas data pipeline |
pandas_portfolio_tracker.py | Portfolio tracking with Pandas |
pandas_time_series.py | Time series analysis |
async_sgd_trainer.py | Async SGD with await sleep() |
Cryptographic Patterns
| Contract | Description |
|---|---|
hash_registry.py | Commit-reveal scheme using SHA-256 |
merkle_airdrop.py | Gas-efficient airdrop with Merkle proofs |
multisig_wallet.py | M-of-N multisig with HMAC verification |
secret_auction.py | Sealed-bid auction with Pedersen commitments |
timelock_vault.py | Time-locked vault with derived-key withdrawal |
Design Patterns
| Contract | Description |
|---|---|
ownable.py | Ownership pattern with two-step transfer |
pausable.py | Pausable contract (inherits Ownable) |
access_control.py | Role-based access control (RBAC) |
registry.py | On-chain contract discovery and metadata |
escrow.py | Escrow with cross-contract PRC-20 transfers |
dao_voting.py | DAO governance with token-weighted voting |
Privacy and ZK
| Contract | Description |
|---|---|
zkp_membership.py | Anonymous authorization via Merkle proofs and nullifiers |
private_model.py | Private ML model inference |
private_statistics.py | Private statistical computations |
differential_privacy.py | Differential privacy mechanisms |
homomorphic_tally.py | Homomorphic encryption for private tallying |
secure_aggregation.py | Secure multi-party aggregation |
federated_trainer.py | Federated learning coordinator |
Visualizations
| Contract | Description |
|---|---|
bar_chart.py | Bar chart via Vega-Lite @view |
dashboard.py | Multi-panel dashboard |
data_table.py | HTML data table |
vega_scatter.py | Scatter plot via Vega-Lite |
Next Steps
- Try the Contract Playground to write and deploy contracts
- Read the Cross-Chain guide for cross-chain messaging patterns
- See the ML Contracts guide for data science on-chain
- Check the Proofs guide for ZK proof integration
- Explore the Visualizations guide for rendering charts from contracts