ASSAY — Technical Documentation

ASSAY is a ZK proof-of-performance protocol for AI trading agents on Robinhood Chain. It takes a wallet's real on-chain execution fills, runs them through a SP1 zkVM program, and writes a cryptographically-verified performance claim to an immutable registry contract — with the underlying strategy kept private.

Architecture

The system is composed of four independently-deployable layers that communicate through well-defined data interfaces:

┌─────────────────────────────────────────────────────────────────┐ │ Robinhood Chain (RHC) │ │ chainId: 46630 (testnet) / 4663 (mainnet) │ │ │ │ ┌──────────────────────┐ ┌──────────────────────────────┐ │ │ │ SP1VerifierPlonk │ │ AssayRegistry │ │ │ │ (v6.1.0 contract) │◄───│ submitClaim(proof, pub) │ │ │ │ verifyProof(...) │ │ claims[wallet][idx] │ │ │ └──────────────────────┘ └──────────────────────────────┘ │ └────────────────────────────────▲────────────────────────────────┘ │ submitClaim tx ┌────────────────────────────────┴────────────────────────────────┐ │ Next.js API (/api/prove) │ │ │ │ 1. fetch fills from Blockscout │ │ 2. serialize to JSON → stdin of 'prove' binary │ │ 3. binary submits to Succinct network prover │ │ 4. poll until proof ready, decode bytes │ │ 5. call AssayRegistry.submitClaim via viem │ └──────────────────────────────────┬──────────────────────────────┘ │ ┌──────────────────────────────────▼──────────────────────────────┐ │ SP1 RISC-V zkVM (zk/program/src/main.rs) │ │ │ │ Private inputs: Vec<Fill> (token transfers from RHC) │ │ Public outputs: AssayClaim { pnl_bp, max_dd_bp, ... } │ │ fillsAnchor = keccak256(tx_hashes) │ │ │ │ Succinct network prover (PROVE token, rpc.mainnet.succinct.xyz)│ └─────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────┐ │ Ingestion (@assay/ingestion) │ │ │ │ blockscout.ts → fetchFills(wallet, from, to) │ │ normalize.ts → Fill[] (multi-leg swap decomposition) │ │ cli.ts → $ assay-ingest --wallet 0x... --out fills.json │ └──────────────────────────────────────────────────────────────────┘

Trust model

Every component is designed to eliminate self-reporting:

  • Fill data — sourced exclusively from Blockscout's RHC API, which reads directly from chain state. No user-submitted data enters the ZK circuit.
  • Computation — executed inside SP1's RISC-V zkVM. The program correctness is guaranteed by the cryptographic proof, not by trust in ASSAY.
  • fillsAnchor — a keccak256 hash over the ordered list of(txHash, blockNumber) pairs, computed inside the circuit. This binds the proof to a specific set of on-chain transactions that anyone can re-verify.
  • Registry — an append-only Solidity contract. Claims are immutable once written. No admin key can delete or modify them.
Design rationaleAn empty registry is more credible than a fabricated one. ASSAY never displays synthetic metrics. Proof #0000 will be the first real proof submitted.

Quickstart

Prerequisites: Node.js ≥ 20, pnpm ≥ 9, Rust 1.78+, Foundry, SP1 toolchain 6.3.1.

1. Clone and install

git clone https://github.com/your-org/assay
cd assay
pnpm install

2. Configure environment

cp .env.example .env
# Fill in at minimum:
#   DEPLOYER_PRIVATE_KEY   — wallet with testnet ETH on RHC 46630
#   BLOCKSCOUT_API_KEY     — Blockscout Pro API key
#   SP1_PROVER             — "mock" for local testing, "network" for Succinct
#   NETWORK_PRIVATE_KEY    — wallet holding PROVE tokens (network mode only)
#   NETWORK_RPC_URL        — https://rpc.mainnet.succinct.xyz

3. Deploy contracts (testnet)

cd contracts
# Deploy SP1 Plonk verifier
forge script script/DeploySP1PlonkVerifier.s.sol \
  --rpc-url https://testnet.rpc.robinhood.com \
  --broadcast --verify

# Deploy AssayRegistry
forge script script/DeployRegistry.s.sol \
  --rpc-url https://testnet.rpc.robinhood.com \
  --broadcast

4. Build the prover binary

cd zk/script
# Requires Go ≥ 1.22, libclang-dev (Linux)
export LIBCLANG_PATH=/usr/lib/llvm-18/lib
cargo build --release --bin prove

5. Run the web app

cd web
pnpm dev
# Open http://localhost:3000

Local development

The monorepo uses pnpm workspaces. Packages:

PathPackageDescription
packages/shared@assay/sharedChain constants, TypeScript types
packages/ingestion@assay/ingestionBlockscout client + fill normalizer
web/Next.js 15 frontend + /api/prove route
contracts/Foundry project (Solidity)
zk/programSP1 guest program (Rust)
zk/scriptSP1 host binaries: prove, spike, vkey

To work on just the frontend without the ZK prover, set SP1_PROVER=mockin web/.env.local. The API route will generate a mock proof immediately.

Environment variables

VariableRequiredDescription
DEPLOYER_PRIVATE_KEYYesEVM private key for contract deployment and claim submission
SP1_VERIFIER_ADDRESSYesAddress of deployed SP1VerifierPlonk contract
ASSAY_REGISTRY_ADDRESSYesAddress of deployed AssayRegistry contract
ASSAY_PROGRAM_VKEYYesSP1 program verification key (output of vkey binary)
BLOCKSCOUT_API_KEYYesBlockscout Pro API key for fill ingestion
SP1_PROVERYesmock | cpu | network
NETWORK_PRIVATE_KEYNetwork onlyWallet holding PROVE tokens on Succinct network
NETWORK_RPC_URLNetwork onlyhttps://rpc.mainnet.succinct.xyz
PROVER_BINNoAbsolute path to pre-built prove binary (default: auto-detected)
REPO_ROOTNoAbsolute path to monorepo root (default: cwd/../)
NEXT_PUBLIC_ASSAY_REGISTRY_ADDRESSFrontendRegistry address exposed to browser

Fills & ingestion

A Fill represents a single completed trade leg on Robinhood Chain — a token transfer event emitted by the DEX router or vault contract. ASSAY ingests these via the Blockscout token-transfers API and normalizes multi-leg swaps into net token flows per wallet.

Fill type

// packages/shared/src/types.ts
export interface Fill {
  txHash:      string;   // 0x-prefixed transaction hash
  blockNumber: number;   // RHC block number
  timestamp:   number;   // Unix seconds
  wallet:      string;   // 0x-prefixed wallet address
  tokenIn:     string;   // contract address of input token
  tokenOut:    string;   // contract address of output token
  amountIn:    bigint;   // raw amount (before decimals)
  amountOut:   bigint;   // raw amount (before decimals)
  decimalsIn:  number;
  decimalsOut: number;
}

Ingestion CLI

# Fetch and normalize fills for a wallet over a block range
npx assay-ingest \
  --wallet   0xYourWallet \
  --from     1200000 \
  --to       1400000 \
  --out      fills.json \
  --chain    46630

Options

FlagDefaultDescription
--walletrequiredEVM wallet address to query
--from0Start block (inclusive)
--tolatestEnd block (inclusive)
--outstdoutOutput file path for fills JSON array
--chain46630Chain ID (46630 testnet, 4663 mainnet)

Normalization logic

Raw Blockscout token transfers are grouped by transaction hash. Within each transaction, ASSAY computes net token flow for the target wallet:

// Pseudocode (normalize.ts)
for each tx in transfers:
  group events by txHash
  for each tokenAddress:
    net = sum(received) - sum(sent)
  if net_usdc < 0 && net_stock > 0:
    emit Fill(USDC→STOCK, amountIn=-net_usdc, amountOut=net_stock)
  elif net_stock < 0 && net_usdc > 0:
    emit Fill(STOCK→USDC, amountIn=-net_stock, amountOut=net_usdc)

ZK proof pipeline

ASSAY uses SP1 v6.3.1 — Succinct's RISC-V zkVM — to generate cryptographic proofs of trading performance. The guest program is standard Rust compiled to a RISC-V target; no custom circuit language required.

Fills JSON ──► SP1 stdin (private) │ ┌─────▼──────────────────────────────────────┐ │ zk/program/src/main.rs (guest) │ │ │ │ 1. read_vec::<Fill>() from stdin │ │ 2. verify fillsAnchor commitment │ │ 3. compute total_pnl_bp │ │ = (final_value - initial_cost) │ │ / initial_cost × 10_000 │ │ 4. compute max_drawdown_bp │ │ 5. check risk limits │ │ 6. commit AssayClaim to stdout (public) │ └─────────────────────────────────────────┬──┘ │ ┌─────────────────────────────────────────▼──┐ │ Succinct network prover │ │ client.prove(&pk, stdin).plonk().run() │ │ → SP1ProofWithPublicValues │ └────────────────────────────────────────────┘

Guest program inputs / outputs

FieldDirectionTypeDescription
fillsPrivate stdinVec<Fill>Serialized trade fills (borsh/bincode)
walletPrivate stdin[u8; 20]EVM wallet address
from_blockPrivate stdinu64Start block of window
to_blockPrivate stdinu64End block of window
AssayClaimPublic outputstructCommitted claim — see below

Proof scheme: Plonk

ASSAY uses Plonk (not Groth16) because it requires no per-circuit trusted setup. The SP1 Plonk verifier contract handles on-chain verification. Gas cost is approximately 400k — one-time per claim.

Anchoring

The fillsAnchor is the crux of ASSAY's security model. It prevents an agent from proving performance over an invented set of trades by binding the ZK proof to a specific sequence of real on-chain transactions.

// Computed inside the ZK guest program
let mut sorted = fills.clone();
sorted.sort_by_key(|f| (f.block_number, f.tx_hash));

let anchor_input: Vec<u8> = sorted
  .iter()
  .flat_map(|f| {
    let mut b = f.tx_hash.to_vec();   // 32 bytes
    b.extend(f.block_number.to_be_bytes()); // 8 bytes
    b
  })
  .collect();

let fills_anchor = keccak256(&anchor_input); // [u8; 32]

The anchor is included in the AssayClaim public output, and stored on-chain in the AssayRegistry. Anyone can re-derive it by fetching the same fills from Blockscout and hashing them identically.

AssayClaim schema

The public output of the ZK program, ABI-encoded and stored on-chain:

// Solidity ABI (AssayClaim public values layout)
struct AssayClaim {
  address wallet;        // trader wallet
  uint64  fromBlock;     // window start block
  uint64  toBlock;       // window end block
  int32   pnlBp;         // PnL in basis points (e.g. 500 = +5.00%)
  uint32  maxDrawdownBp; // max drawdown in basis points
  bool    withinRisk;    // all fills within declared risk limits
  bytes32 fillsAnchor;   // keccak256 of (txHash || blockNumber) list
}

Rust representation

// zk/program/src/main.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssayClaim {
  pub wallet:         [u8; 20],
  pub from_block:     u64,
  pub to_block:       u64,
  pub pnl_bp:         i32,
  pub max_drawdown_bp: u32,
  pub within_risk:    bool,
  pub fills_anchor:   [u8; 32],
}

Smart Contracts

All contracts are compiled with Solidity 0.8.20 via Foundry. The repository is at contracts/.

AssayRegistry

Append-only registry for verified performance claims. No owner, no upgradability, no admin functions.

// SPDX-License-Identifier: MIT
interface IAssayRegistry {
  struct Claim {
    address wallet;
    uint64  fromBlock;
    uint64  toBlock;
    int32   pnlBp;
    uint32  maxDrawdownBp;
    bool    withinRisk;
    bytes32 fillsAnchor;
    uint256 timestamp;
  }

  /// Submit a ZK-verified performance claim.
  /// Reverts if the SP1 proof is invalid.
  function submitClaim(
    bytes calldata proof,
    bytes calldata publicValues
  ) external;

  /// Return the number of claims for a wallet.
  function claimCount(address wallet) external view returns (uint256);

  /// Return a specific claim by index.
  function getClaim(address wallet, uint256 index)
    external view returns (Claim memory);

  /// Return all claims for a wallet.
  function getClaims(address wallet)
    external view returns (Claim[] memory);

  event ClaimSubmitted(
    address indexed wallet,
    int32   pnlBp,
    bytes32 fillsAnchor,
    uint256 index
  );
}

Verification flow

submitClaim calls ISP1Verifier.verifyProof(vkey, publicValues, proofBytes)internally. If the call reverts, the transaction reverts. A claim is only written to storage if the Plonk proof is valid against the registered program vkey.

SP1VerifierPlonk

Standard SP1 verifier contract from Succinct Labs, version 6.1.0, Plonk variant. Source: @sp1-contracts/v6.1.0/SP1VerifierPlonk.sol. No modifications.

interface ISP1Verifier {
  function verifyProof(
    bytes32        programVKey,
    bytes calldata publicValues,
    bytes calldata proofBytes
  ) external view;
}

Deployed addresses

Testnet (chainId 46630):

SP1VerifierPlonk0xd47C8eb8CD655a978D6170e6D9615711B33bB883
AssayRegistrySee ASSAY_REGISTRY_ADDRESS in .env
Program VKeySee ASSAY_PROGRAM_VKEY in .env
RPChttps://testnet.rpc.robinhood.com
Explorerhttps://robinhoodchain.blockscout.com

API Reference

POST /api/prove

Orchestrates the full proof pipeline: ingest fills → generate ZK proof → submit on-chain. This is a server-side Next.js route handler running on the VPS.

Request

POST /api/prove
Content-Type: application/json

{
  "wallet":     "0xYourWalletAddress",
  "fromBlock":  1200000,
  "toBlock":    1400000
}

Response — success

HTTP 200
{
  "ok":          true,
  "txHash":      "0xabc...def",
  "claimIndex":  0,
  "claim": {
    "wallet":        "0x...",
    "fromBlock":     1200000,
    "toBlock":       1400000,
    "pnlBp":         312,
    "maxDrawdownBp": 87,
    "withinRisk":    true,
    "fillsAnchor":   "0x..."
  }
}

Response — error

HTTP 400 / 500
{
  "ok":    false,
  "error": "No fills found for wallet in block range"
}

Pipeline internals

// web/app/api/prove/route.ts (simplified)
export async function POST(req: Request) {
  const { wallet, fromBlock, toBlock } = await req.json();

  // 1. Ingest fills from Blockscout
  const fills = await fetchFills(wallet, fromBlock, toBlock, chainId);
  if (!fills.length) throw new Error("No fills found");

  // 2. Write fills.json to tmp
  const fillsPath = writeTmp(fills);

  // 3. Run pre-built 'prove' binary
  const outPath = tmpFile('proof.json');
  await run(PROVER_BIN, ['--input', fillsPath, '--out', outPath], REPO_ROOT);

  // 4. Read proof output
  const { proofBytes, publicValues } = JSON.parse(readFile(outPath));

  // 5. Submit on-chain via viem
  const txHash = await submitClaim(proofBytes, publicValues);
  return Response.json({ ok: true, txHash, ... });
}
Note on latencyProof generation on the Succinct network takes 3–10 minutes depending on circuit complexity and network load. The endpoint is long-running. For production use, implement a job queue with polling (see the Deployment section).

Ingestion CLI

The @assay/ingestion package ships a CLI that can be used independently of the web app to produce fills.json files for any wallet.

# Install globally
npm install -g @assay/ingestion

# Fetch fills
assay-ingest \
  --wallet   0xABCDEF... \
  --from     0 \
  --to       latest \
  --out      ./fills.json

# Output format
[
  {
    "txHash":      "0x...",
    "blockNumber": 1234567,
    "timestamp":   1700000000,
    "wallet":      "0x...",
    "tokenIn":     "0x...",
    "tokenOut":    "0x...",
    "amountIn":    "1000000",
    "amountOut":   "997345",
    "decimalsIn":  6,
    "decimalsOut": 6
  },
  ...
]

Blockscout adapter

Located at packages/ingestion/src/blockscout.ts. Uses the Etherscan-compatible v1 API on robinhoodchain.blockscout.com.

// packages/ingestion/src/blockscout.ts
export async function fetchTokenTransfers(
  wallet:      string,
  startBlock:  number,
  endBlock:    number,
  apiKey?:     string
): Promise<RawTransfer[]>

// Base URL
const BASE = 'https://robinhoodchain.blockscout.com/api/v1'

// Endpoint used
GET /api?module=account&action=tokentx
  &address={wallet}
  &startblock={from}
  &endblock={to}
  &sort=asc
  &apikey={BLOCKSCOUT_API_KEY}

AI Agent Integration

ASSAY is designed to be called by autonomous AI trading agents that want to publish verifiable track records. The minimal integration is a single HTTP call to /api/prove after a trading session ends.

Minimal integration

// TypeScript agent snippet
async function publishProof(wallet: string, fromBlock: number, toBlock: number) {
  const response = await fetch('https://assay.yourapp.com/api/prove', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ wallet, fromBlock, toBlock }),
  });

  const result = await response.json();
  if (!result.ok) throw new Error(result.error);

  console.log("Proof submitted: " + result.txHash);
  console.log("PnL: " + (result.claim.pnlBp / 100) + "%");
  return result;
}

Python agent snippet

import requests

def publish_proof(wallet: str, from_block: int, to_block: int):
    resp = requests.post(
        "https://assay.yourapp.com/api/prove",
        json={"wallet": wallet, "fromBlock": from_block, "toBlock": to_block},
        timeout=600,   # proof takes 3-10 minutes
    )
    resp.raise_for_status()
    data = resp.json()
    print(f"Proof submitted: {data['txHash']}")
    return data

Read claims without the API

// Read directly from AssayRegistry via viem
import { createPublicClient, http } from 'viem';

const client = createPublicClient({
  chain: robinhoodChainTestnet, // chainId: 46630
  transport: http('https://testnet.rpc.robinhood.com'),
});

const claims = await client.readContract({
  address: ASSAY_REGISTRY_ADDRESS,
  abi: assayRegistryAbi,
  functionName: 'getClaims',
  args: [walletAddress],
});

// claims: AssayClaim[]
// claims[i].pnlBp / 100 = PnL percentage

MCP tool specification

ASSAY exposes two MCP (Model Context Protocol) tools for AI assistants and agent frameworks that support the MCP standard.

Tool: assay_prove

{
  "name": "assay_prove",
  "description": "Generate and publish a ZK proof of trading performance for a wallet on Robinhood Chain. Takes 3-10 minutes. Returns transaction hash and claim details.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "wallet":     { "type": "string",  "description": "EVM wallet address (0x...)" },
      "fromBlock":  { "type": "integer", "description": "Start block of the trading window" },
      "toBlock":    { "type": "integer", "description": "End block. Use 0 for latest." }
    },
    "required": ["wallet", "fromBlock"]
  }
}

Tool: assay_leaderboard

{
  "name": "assay_leaderboard",
  "description": "Query the AssayRegistry for verified performance claims. Returns a list of claims sorted by PnL.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "wallet": {
        "type": "string",
        "description": "Optional. Filter by wallet address."
      },
      "minPnlBp": {
        "type": "integer",
        "description": "Optional. Minimum PnL in basis points (e.g. 100 = 1%)."
      },
      "limit": {
        "type": "integer",
        "description": "Max results to return. Default 20."
      }
    }
  }
}
MCP server statusThe MCP server is not yet deployed. The tool specs above are the planned interface. Until then, agents should call the REST API directly.

Leaderboard query

The leaderboard is fully on-chain. You can query it using any EVM JSON-RPC client or via the AssayRegistry ABI without going through ASSAY's server.

// Fetch top performers via Blockscout event logs
// Listen for ClaimSubmitted(indexed wallet, pnlBp, fillsAnchor, index)
const logs = await client.getLogs({
  address: ASSAY_REGISTRY_ADDRESS,
  event: parseAbiItem(
    'event ClaimSubmitted(address indexed wallet, int32 pnlBp, bytes32 fillsAnchor, uint256 index)'
  ),
  fromBlock: 0n,
  toBlock: 'latest',
});

// Sort by pnlBp descending
const leaderboard = logs
  .map(log => ({
    wallet: log.args.wallet,
    pnlBp:  log.args.pnlBp,
    anchor: log.args.fillsAnchor,
  }))
  .sort((a, b) => Number(b.pnlBp) - Number(a.pnlBp));

Succinct network prover

In production, ASSAY uses Succinct's hosted prover network instead of running SP1 locally. This reduces proof generation from ~3 hours (CPU) to ~5 minutes.

Requirements

  • A wallet with PROVE tokens (on Ethereum mainnet or BNB Chain)
  • Deposit PROVE tokens to the Succinct network via network.succinct.xyz
  • Set SP1_PROVER=network and NETWORK_PRIVATE_KEY in env

Configuration

# .env
SP1_PROVER=network
NETWORK_PRIVATE_KEY=0xYourPrivateKey   # holds PROVE tokens
NETWORK_RPC_URL=https://rpc.mainnet.succinct.xyz
ImportantThe PROVE token lives on Ethereum/BNB, not on Robinhood Chain. This is fine — the prover payment is separate from RHC gas. You need ETH on RHC for thesubmitClaim transaction gas only.

VPS deployment

The production setup runs on a Linux VPS with nginx as reverse proxy.

Process

# 1. Build the prove binary (do this on the VPS, needs Go + libclang)
export LIBCLANG_PATH=/usr/lib/llvm-18/lib
export PATH="$PATH:/usr/local/go/bin"
cd /root/assay/zk/script
cargo build --release --bin prove

# 2. Build Next.js
cd /root/assay/web
pnpm build

# 3. Start with PM2 (or systemd)
pm2 start "pnpm start" --name assay-web --cwd /root/assay/web

# 4. Nginx config fragment
server {
  listen 80;
  server_name your-domain.com;
  location / {
    proxy_pass         http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header   Upgrade $http_upgrade;
    proxy_set_header   Connection 'upgrade';
    proxy_set_header   Host $host;
    proxy_read_timeout 900s;   # proof can take up to 15 min
  }
}
proxy_read_timeoutSet this to at least 900 seconds. The /api/prove route is synchronous and holds the HTTP connection open until the proof is submitted on-chain.

Security notes

  • Private key exposureDEPLOYER_PRIVATE_KEY andNETWORK_PRIVATE_KEY are loaded by the Next.js API route. They are never exposed to the browser. Use a minimal-balance wallet for the deployer.
  • Registry immutability — once a claim is submitted, it cannot be deleted. The registry has no owner or upgrade mechanism.
  • Replay protection — the fillsAnchor uniquely identifies a proof. Submitting the same proof twice will succeed as a second claim entry (it does not overwrite). Use claimCount(wallet) to detect duplicates before submitting.
  • fillsAnchor verification — any third party can verify a claim by fetching the same block range from Blockscout, hashing the fills identically, and comparing against the stored fillsAnchor.
  • SP1 program vkey — the ASSAY_PROGRAM_VKEY is baked into the AssayRegistry at deployment time. Upgrading the ZK program requires redeploying the registry with a new vkey.
← Back to landingLaunch AppASSAY · ZK proof-of-performance · Robinhood Chain