API Reference
Panda extends the standard Ethereum JSON-RPC and Solana JSON-RPC APIs with custom methods for Python smart contract management. This document covers all Panda-specific API extensions.
For contract authors: the JSON-RPC methods below are the low-level wire format. They take Python source as a string because RPC payloads are strings — that's a transport detail, not how you should be authoring or deploying contracts in day-to-day work. Contracts are files on disk. Write your contract as a
.pyfile, test it with thepanda-sdktest runner, then deploy that file unchanged via thepanda-sdk-clientPandaProvideror thepanda deploy contract.pyCLI. Never inline contract source in your application code. This page is for SDK / tooling authors who need to know what the wire looks like.
Ethereum JSON-RPC Extensions
These methods are available on panda-geth RPC endpoints (default port 8545). They supplement the standard eth_*, net_*, and web3_* methods.
panda_deployContract
Simulate deploying a Python smart contract against the latest state snapshot. This method runs the deploy (including the @constructor) and returns the resulting address and gas, but it does not commit state to the chain — it's for simulation and dry-run validation. To persist a contract, send an eth_sendTransaction to the Panda deploy precompile (0x000000000000000000000000000050414E444101); see Deploying for real.
Parameters (single object):
| Name | Type | Description |
|---|---|---|
code | string | Python source code of the contract |
constructor_args | string | JSON-encoded constructor arguments passed to the @constructor method (optional; defaults to "{}") |
from | string | Address of the deployer |
Returns: Object with address, txHash, and gasUsed.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_deployContract",
"params": [{
"code": "from panda import contract, call, query\n\n@contract\nclass Counter:\n class State:\n count: int = 0\n\n @call\n def increment(self, ctx, amount: int = 1):\n self.state.count += amount\n\n @query\n def get_count(self) -> int:\n return self.state.count",
"constructor_args": "{}",
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
}],
"id": 1
}
// Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"address": "0x1234567890abcdef1234567890abcdef12345678",
"txHash": "",
"gasUsed": 84210
}
}
panda_callContract
Execute a state-mutating (@call) method against the latest state snapshot and return its result, events, and gas. Like panda_deployContract, this runs against a snapshot and does not commit state changes to the chain — use it for simulation. To persist a @call, send an eth_sendTransaction to the contract address; see Calling for real.
Parameters (single object):
| Name | Type | Description |
|---|---|---|
address | string | Contract address |
method | string | Method name to call |
args | string | JSON-encoded method arguments (key-value pairs) |
from | string | Caller address |
Returns: Object with returnValue (JSON), gasUsed, events, and logs.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_callContract",
"params": [{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"method": "increment",
"args": "{\"amount\": 5}",
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
}],
"id": 2
}
// Response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"returnValue": null,
"gasUsed": 14820,
"events": [],
"logs": []
}
}
panda_queryContract
Call a read-only (@query) method on a deployed contract. Does not produce a transaction and does not mutate state.
Parameters (single object):
| Name | Type | Description |
|---|---|---|
address | string | Contract address |
method | string | Method name to query |
args | string | JSON-encoded method arguments (optional; defaults to "{}") |
Returns: Object with returnValue (the method's return value, JSON), gasUsed, and logs.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_queryContract",
"params": [{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"method": "get_count",
"args": "{}"
}],
"id": 3
}
// Response
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"returnValue": 5,
"gasUsed": 920,
"logs": []
}
}
panda_getContractState
Retrieve the full state of a deployed contract.
Parameters (positional):
| Position | Type | Description |
|---|---|---|
address | string | Contract address |
Returns: Object containing all state fields and their current values.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_getContractState",
"params": ["0x1234567890abcdef1234567890abcdef12345678"],
"id": 4
}
// Response
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"count": 5,
"owner": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
}
}
panda_getContractCode
Retrieve the Python source code of a deployed contract.
Parameters:
| Name | Type | Description |
|---|---|---|
address | string | Contract address |
Returns: Python source code (string)
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_getContractCode",
"params": ["0x1234567890abcdef1234567890abcdef12345678"],
"id": 5
}
// Response
{
"jsonrpc": "2.0",
"id": 5,
"result": "from panda import contract, call, query\n\n@contract\nclass Counter:\n class State:\n count: int = 0\n..."
}
panda_lintContract
Validate a Python smart contract without deploying it. Runs the same static-analysis and determinism checks the VM applies at deploy time.
Parameters (positional):
| Position | Type | Description |
|---|---|---|
code | string | Python source code to lint |
Returns: Object with valid (boolean), errors (array of strings, omitted when empty), and warnings (array of strings, omitted when empty). When valid is false, errors contains one or more human-readable validation messages.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_lintContract",
"params": ["from panda import contract, call\nimport os\n\n@contract\nclass Bad:\n class State:\n x: int = 0\n @call\n def hack(self, ctx):\n os.system('whoami')"],
"id": 6
}
// Response
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"valid": false,
"errors": [
"Forbidden import: 'os' is not allowed in Panda contracts"
]
}
}
panda_estimateGas
Estimate the gas cost of a contract method by running it read-only against the latest state. Returns both the native Panda gas estimate and its EVM-gas equivalent.
Parameters (single object):
| Name | Type | Description |
|---|---|---|
address | string | Contract address |
method | string | Method name |
args | string | JSON-encoded method arguments |
from | string | Caller address (optional) |
Returns: Object with gasEstimate (native Panda gas) and evmGasEquivalent (EVM gas).
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_estimateGas",
"params": [{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"method": "train",
"args": "{\"features\": [[1.0, 2.0], [3.0, 4.0]], \"labels\": [1.0, 2.0]}"
}],
"id": 7
}
// Response
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"gasEstimate": 142857,
"evmGasEquivalent": 1438609
}
}
panda_getProof
Retrieve the ZK proof for a transaction that required one.
Parameters (single object):
| Name | Type | Description |
|---|---|---|
txHash | string | Transaction hash |
Returns: Object with txHash, proofData (hex-encoded), publicInputs (hex-encoded), proofSystem (e.g. risc_zero), and verified.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_getProof",
"params": [{"txHash": "0xabc123..."}],
"id": 8
}
// Response
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"txHash": "0xabc123...",
"proofData": "0x...",
"publicInputs": "0x...",
"proofSystem": "risc_zero",
"verified": true
}
}
panda_verifyProof
Verify a ZK proof on-chain.
Parameters (single object):
| Name | Type | Description |
|---|---|---|
proofData | string | Hex-encoded proof bytes (as returned in panda_getProof's proofData) |
publicInputs | string | Hex-encoded public inputs |
proofSystem | string | Proof system (optional; defaults to risc_zero) |
Returns: Object with valid (boolean) and error (string, present only on failure).
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_verifyProof",
"params": [{"proofData": "0x...", "publicInputs": "0x...", "proofSystem": "risc_zero"}],
"id": 9
}
// Response
{
"jsonrpc": "2.0",
"id": 9,
"result": {
"valid": true
}
}
panda_renderView
Execute a @view method on a deployed contract and return its rendered content (read-only). Used by the explorer and notebook to render visualizations (Vega-Lite, SVG, HTML, images).
Parameters (single object):
| Name | Type | Description |
|---|---|---|
address | string | Contract address |
method | string | @view method name |
args | object | Method arguments as a JSON object (optional) |
Returns: Object with content, contentType, gasUsed, and isView.
Example:
// Request
{
"jsonrpc": "2.0",
"method": "panda_renderView",
"params": [{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"method": "chart"
}],
"id": 10
}
// Response
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"content": {"...": "vega-lite spec"},
"contentType": "application/vnd.vega-lite+json",
"gasUsed": 5120,
"isView": true
}
}
The panda namespace also exposes system-contract registry methods used by the explorer:
panda_registerSystemContract,panda_getSystemContract,panda_listSystemContracts, andpanda_unregisterSystemContract.
Deploying and calling for real (eth_sendTransaction)
The panda_deployContract and panda_callContract methods above run against a state snapshot and do not commit changes to the chain — they exist for simulation, gas estimation, and linting. To persist a deploy or a @call, send a standard eth_sendTransaction; the transaction pipeline commits state during block processing.
Deploying for real
Send the encoded deploy payload to the Panda deploy precompile at 0x000000000000000000000000000050414E444101. The data field is the hex-encoded JSON {"code": "<python source>", "constructor_args": "<json string>"} (prefixed with 0x):
{
"jsonrpc": "2.0",
"method": "eth_sendTransaction",
"params": [{
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
"to": "0x000000000000000000000000000050414E444101",
"data": "0x...",
"gas": "0x2DC6C0"
}],
"id": 1
}
After the transaction is mined, fetch the receipt with eth_getTransactionReceipt. The deployed contract address is emitted by the PandaContractDeployed event — take the last 40 hex characters of logs[0].topics[1] and prefix with 0x.
Calling for real
For a state-mutating @call, send the encoded call payload directly to the contract address (not the precompile). The data field is the hex-encoded JSON {"method": "<method_name>", "args": "<json string>"} (prefixed with 0x):
{
"jsonrpc": "2.0",
"method": "eth_sendTransaction",
"params": [{
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
"to": "0x1234567890abcdef1234567890abcdef12345678",
"data": "0x...",
"gas": "0x100000"
}],
"id": 2
}
@query and @view methods are read-only and never need a transaction — use panda_queryContract / panda_renderView instead.
Solana Program Interface
Panda on Solana is not a set of custom JSON-RPC methods. It is the Panda Loader, a builtin program (program id PandaLoader1111111111111111111111111111111) that executes Python contracts through PandaVM, the same way the BPF loader executes BPF bytecode.
You interact with it using standard Solana RPC only:
sendTransaction— deploy, call (state-mutating), upgrade, and process-timers.simulateTransaction— query (read-only); read the method's return value from the simulated transaction's return data.getAccountInfo— read raw contract state or source code straight from the relevant account.
There are no pandaDeployProgram, pandaCallProgram, pandaQueryProgram, pandaGetProgramState, or pandaGetProgramCode methods. (The panda_* JSON-RPC extensions documented above are Ethereum-only.)
Instruction format
Every Panda Loader instruction is encoded as:
data = [discriminant byte] || payload
The first byte selects the operation; the remaining bytes are its payload.
| Disc | Instruction | Payload | Accounts (in order) |
|---|---|---|---|
0 | Deploy | UTF-8 Python source | [signer, program, state_pda] |
1 | Call | JSON {"method":"...","args":"{...}"} | [signer, program, state_pda, (timer_pda), (extra contract pairs)] |
2 | Query | JSON {"method":"...","args":"{...}"} | [program, state_pda] — via simulateTransaction |
3 | Upgrade | new UTF-8 Python source | [authority_signer, program] |
4 | ProcessTimers | empty | [program, state_pda, timer_pda] |
args is itself a JSON string (a serialized object), matching the Ethereum call payload shape. For Call, the timer_pda and any additional contract account pairs (program, state for cross-contract calls) are appended after the state PDA when needed. Upgrade requires the original deploy authority to sign.
Program-derived addresses (PDAs)
State and timer accounts are PDAs derived from the contract's program address under the Panda Loader program id, using Solana's standard findProgramAddress algorithm:
| Account | Seeds |
|---|---|
| State PDA | ["panda_state", programAddress] |
| Timer PDA | ["panda_timers", programAddress] |
| Event PDA | ["panda_events", programAddress] |
const [statePda] = PublicKey.findProgramAddressSync(
[Buffer.from("panda_state"), programAddress.toBuffer()],
PANDA_LOADER_PROGRAM_ID,
);
Reading the return value
A method's JSON return value is placed in the transaction's return data via set_return_data. Read it back from the returnData field of a confirmed transaction (getTransaction) or, for queries, from the result of simulateTransaction:
// Request — read-only query via simulation
{
"jsonrpc": "2.0",
"method": "simulateTransaction",
"params": [
"<base64-encoded transaction with a Query (disc 2) instruction>",
{"encoding": "base64", "sigVerify": false}
],
"id": 1
}
// Response — return value is JSON in returnData (base64)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": {"slot": 12345},
"value": {
"err": null,
"logs": ["Program PandaLoader1111111111111111111111111111111 invoke [1]"],
"returnData": {
"programId": "PandaLoader1111111111111111111111111111111",
"data": ["MTA=", "base64"]
}
}
}
}
Here returnData.data[0] is base64; decoding MTA= yields the JSON 10.
Reading state and code directly
To inspect a contract's stored state or its Python source, call standard getAccountInfo — no Panda-specific method is involved.
- State lives in the state PDA; its data is the raw serialized state bytes (MessagePack or JSON) produced by PandaVM.
- Source code lives in the program account, laid out as:
[0] : u8 — executable flag (0x00 / 0x01)
[1..33] : [u8; 32] — deploy authority pubkey
[33..] : UTF-8 Python source code
Strip the 33-byte header from the program account data to recover the contract source.
Compute units
PandaVM gas is converted to Solana compute units at a fixed ratio:
1 PandaVM gas unit = 5 Solana compute units
Per-instruction gas limits inside the loader:
| Instruction | PandaVM gas limit |
|---|---|
| Deploy | 2,000,000 |
| Call | 1,000,000 |
| Query | 500,000 |
Error Codes
Both Ethereum and Solana RPC extensions use consistent error codes:
| Code | Name | Description |
|---|---|---|
| -32000 | EXECUTION_ERROR | Contract execution failed (Python exception) |
| -32001 | LINT_ERROR | Contract failed linting/validation |
| -32002 | OUT_OF_GAS | Execution exceeded gas limit |
| -32003 | TIMEOUT | Execution exceeded wall-clock timeout |
| -32004 | SANDBOX_VIOLATION | Contract attempted forbidden operation |
| -32005 | STATE_ERROR | State read/write failure |
| -32006 | NOT_A_PANDA_CONTRACT | Address is not a Panda contract |
| -32007 | METHOD_NOT_FOUND | Specified method does not exist on contract |
| -32008 | PROOF_ERROR | ZK proof generation or verification failed |
| -32009 | DEPLOYMENT_ERROR | Contract deployment failed |
| -32010 | DETERMINISM_ERROR | Non-deterministic execution detected |
Example error response:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Contract execution failed",
"data": {
"type": "AssertionError",
"message": "Insufficient balance",
"traceback": " File \"contract.py\", line 15, in transfer\n assert balance >= amount, \"Insufficient balance\"",
"gasUsed": 12345
}
}
}
WebSocket Subscriptions
Ethereum WebSocket (port 8546)
Subscribe to Panda-specific events via WebSocket:
// Subscribe to all Panda contract events
{
"jsonrpc": "2.0",
"method": "eth_subscribe",
"params": ["logs", {
"topics": ["0xPANDA_EVENT_TOPIC"]
}],
"id": 1
}
// Subscribe to specific contract events
{
"jsonrpc": "2.0",
"method": "eth_subscribe",
"params": ["logs", {
"address": "0x1234567890abcdef1234567890abcdef12345678",
"topics": ["0xPANDA_EVENT_TOPIC"]
}],
"id": 2
}
Solana WebSocket (port 8900)
Use standard Solana WebSocket subscriptions to watch Panda program activity:
// Subscribe to program account changes
{
"jsonrpc": "2.0",
"method": "programSubscribe",
"params": [
"PNDAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
{"encoding": "jsonParsed"}
],
"id": 1
}
Rate Limits
Default rate limits (configurable per deployment):
| Endpoint Type | Limit |
|---|---|
Query methods (panda_queryContract, Solana simulateTransaction) | 100 req/sec |
Transaction methods (panda_callContract, Solana sendTransaction) | 20 req/sec |
Lint (panda_lintContract) | 10 req/sec |
| Gas estimation | 50 req/sec |
| State queries | 100 req/sec |
| WebSocket subscriptions | 50 concurrent per connection |
SDK Integration
Python
import requests
import json
PANDA_PRECOMPILE = "0x000000000000000000000000000050414E444101"
def encode_deploy_payload(code, constructor_args=None):
"""Hex-encode the deploy payload for eth_sendTransaction."""
args_str = json.dumps(constructor_args) if constructor_args else "{}"
payload = json.dumps({"code": code, "constructor_args": args_str})
return "0x" + payload.encode("utf-8").hex()
def encode_call_payload(method, args=None):
"""Hex-encode the call payload for eth_sendTransaction."""
args_str = json.dumps(args) if args else "{}"
payload = json.dumps({"method": method, "args": args_str})
return "0x" + payload.encode("utf-8").hex()
# Illustrative only: this shows the raw JSON-RPC wire calls. For application
# code use the official client `PandaProvider` (pip install panda-sdk-client),
# which handles payload encoding, receipt polling, and address extraction.
class RawPandaRpc:
def __init__(self, rpc_url="http://localhost:8545"):
self.rpc_url = rpc_url
self._id = 0
def _call(self, method, params):
self._id += 1
response = requests.post(self.rpc_url, json={
"jsonrpc": "2.0",
"method": method,
"params": [params] if isinstance(params, dict) else params,
"id": self._id,
})
result = response.json()
if "error" in result:
raise Exception(f"RPC Error: {result['error']['message']}")
return result["result"]
def deploy_file(self, contract_path, from_addr, gas="0x2DC6C0"):
"""Deploy a contract read from a `.py` file on disk.
Write your contract as a `.py` file, run tests against it, then
deploy the same file unchanged -- read the source and pass it to
the deploy payload rather than constructing an inline string.
"""
with open(contract_path, "r", encoding="utf-8") as f:
code = f.read()
# A real deploy persists state via eth_sendTransaction to the Panda
# deploy precompile. `data` is the encoded deploy payload (gzipped
# source + constructor args) -- see the official panda-client SDKs
# for the exact encoding.
return self._call("eth_sendTransaction", [{
"from": from_addr,
"to": PANDA_PRECOMPILE,
"data": encode_deploy_payload(code),
"gas": gas,
}])
def call(self, address, method, args=None, from_addr=None, gas="0x100000"):
# A real @call persists state via eth_sendTransaction to the contract
# address. `data` is the encoded call payload.
return self._call("eth_sendTransaction", [{
"from": from_addr,
"to": address,
"data": encode_call_payload(method, args or {}),
"gas": gas,
}])
def query(self, address, method, args=None):
result = self._call("panda_queryContract", [{
"address": address,
"method": method,
"args": json.dumps(args or {}),
}])
return result["returnValue"]
def get_state(self, address):
return self._call("panda_getContractState", [address])
def lint(self, code):
return self._call("panda_lintContract", [code])
# Usage (raw wire calls -- for SDK authors)
client = RawPandaRpc("http://localhost:8545")
tx = client.deploy_file("counter.py", from_addr="0x742d35Cc...")
state = client.get_state("0x1234...")
result = client.query("0x1234...", "get_count")
For application code, use the official
panda-sdk-clientPython package:from panda_client import PandaProvider. It handles deploy/call payload encoding, receipt polling, and contract-address extraction (from thePandaContractDeployedevent) for you. The snippet above shows the underlying wire calls those clients make.
You can also deploy from the Rust CLI with
panda deploy counter.py. Whichever path you use, write your contract as a.pyfile and deploy that file unchanged -- never an inline source string.
JavaScript / TypeScript
import { ethers } from "ethers";
const PANDA_PRECOMPILE = "0x000000000000000000000000000050414E444101";
function toHex(s: string): string {
return "0x" + Buffer.from(s, "utf-8").toString("hex");
}
function encodeDeployPayload(code: string, constructorArgs: object = {}): string {
return toHex(JSON.stringify({ code, constructor_args: JSON.stringify(constructorArgs) }));
}
function encodeCallPayload(method: string, args: object = {}): string {
return toHex(JSON.stringify({ method, args: JSON.stringify(args) }));
}
class PandaProvider {
private provider: ethers.JsonRpcProvider;
constructor(rpcUrl: string = "http://localhost:8545") {
this.provider = new ethers.JsonRpcProvider(rpcUrl);
}
/**
* Deploy a contract file from disk.
*
* Application code should call this with a file path, not an inline
* code string. Inline-string deploys are a wire-level detail only
* SDK / tooling authors need to know about.
*/
async deployFile(contractPath: string, from: string): Promise<string> {
const fs = await import("node:fs/promises");
const code = await fs.readFile(contractPath, "utf-8");
// A real deploy persists state via eth_sendTransaction to the Panda
// deploy precompile. `data` is the encoded deploy payload.
return this.provider.send("eth_sendTransaction", [{
from,
to: PANDA_PRECOMPILE,
data: encodeDeployPayload(code),
gas: "0x2DC6C0",
}]);
}
async call(address: string, method: string, args: object = {}): Promise<string> {
// A real @call persists state via eth_sendTransaction to the contract.
return this.provider.send("eth_sendTransaction", [{
from: await this.provider.getSigner().then(s => s.getAddress()),
to: address,
data: encodeCallPayload(method, args),
gas: "0x100000",
}]);
}
async query(address: string, method: string, args: object = {}): Promise<any> {
const result = await this.provider.send("panda_queryContract", [{
address,
method,
args: JSON.stringify(args),
}]);
return result.returnValue;
}
async getState(address: string): Promise<object> {
return this.provider.send("panda_getContractState", [address]);
}
async getCode(address: string): Promise<string> {
return this.provider.send("panda_getContractCode", [address]);
}
async lint(code: string): Promise<object> {
return this.provider.send("panda_lintContract", [code]);
}
}