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 .py file, test it with the panda-sdk test runner, then deploy that file unchanged via the panda-sdk-client PandaProvider or the panda deploy contract.py CLI. 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):

NameTypeDescription
codestringPython source code of the contract
constructor_argsstringJSON-encoded constructor arguments passed to the @constructor method (optional; defaults to "{}")
fromstringAddress 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):

NameTypeDescription
addressstringContract address
methodstringMethod name to call
argsstringJSON-encoded method arguments (key-value pairs)
fromstringCaller 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):

NameTypeDescription
addressstringContract address
methodstringMethod name to query
argsstringJSON-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):

PositionTypeDescription
addressstringContract 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:

NameTypeDescription
addressstringContract 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):

PositionTypeDescription
codestringPython 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):

NameTypeDescription
addressstringContract address
methodstringMethod name
argsstringJSON-encoded method arguments
fromstringCaller 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):

NameTypeDescription
txHashstringTransaction 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):

NameTypeDescription
proofDatastringHex-encoded proof bytes (as returned in panda_getProof's proofData)
publicInputsstringHex-encoded public inputs
proofSystemstringProof 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):

NameTypeDescription
addressstringContract address
methodstring@view method name
argsobjectMethod 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, and panda_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.

DiscInstructionPayloadAccounts (in order)
0DeployUTF-8 Python source[signer, program, state_pda]
1CallJSON {"method":"...","args":"{...}"}[signer, program, state_pda, (timer_pda), (extra contract pairs)]
2QueryJSON {"method":"...","args":"{...}"}[program, state_pda] — via simulateTransaction
3Upgradenew UTF-8 Python source[authority_signer, program]
4ProcessTimersempty[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:

AccountSeeds
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:

InstructionPandaVM gas limit
Deploy2,000,000
Call1,000,000
Query500,000

Error Codes

Both Ethereum and Solana RPC extensions use consistent error codes:

CodeNameDescription
-32000EXECUTION_ERRORContract execution failed (Python exception)
-32001LINT_ERRORContract failed linting/validation
-32002OUT_OF_GASExecution exceeded gas limit
-32003TIMEOUTExecution exceeded wall-clock timeout
-32004SANDBOX_VIOLATIONContract attempted forbidden operation
-32005STATE_ERRORState read/write failure
-32006NOT_A_PANDA_CONTRACTAddress is not a Panda contract
-32007METHOD_NOT_FOUNDSpecified method does not exist on contract
-32008PROOF_ERRORZK proof generation or verification failed
-32009DEPLOYMENT_ERRORContract deployment failed
-32010DETERMINISM_ERRORNon-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 TypeLimit
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 estimation50 req/sec
State queries100 req/sec
WebSocket subscriptions50 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-client Python package: from panda_client import PandaProvider. It handles deploy/call payload encoding, receipt polling, and contract-address extraction (from the PandaContractDeployed event) 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 .py file 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]);
  }
}