Introduction

Rig Onchain Kit is a robust framework for building AI-powered applications that interact natively with blockchain networks. Combining the cognitive capabilities of large language models with secure blockchain operations, this toolkit enables developers to create intelligent agents capable of executing complex on-chain interactions across both Solana and EVM-compatible networks.

At its core, Rig Onchain Kit merges:

  • The rig-core AI agent framework for natural language processing and decision-making
  • The listen blockchain library for Solana and EVM transaction orchestration
  • A production-ready HTTP service with real-time streaming capabilities
  • Secure multi-chain wallet management through Privy integration

The toolkit provides pre-built agents equipped with essential blockchain operations including token swaps (via Jupiter and Uniswap), asset transfers, balance queries, and smart contract interactions. Developers can extend functionality using the #[tool] macro system to create custom operations while maintaining strict security boundaries through the SignerContext architecture.

Key differentiators:

  • Dual-chain First - Native support for Solana and EVM ecosystems with automatic RPC configuration
  • Secure by Design - Thread-local signer isolation and Privy-based authentication for production deployments
  • Real-time Streaming - SSE-enabled HTTP service handles concurrent user sessions with tool call transparency
  • Extensible Tool System - Combine prebuilt DeFi operations with custom logic through macro-driven tool creation
  • Wallet Agnostic - Supports both local key management and Privy-embedded wallets for user-friendly onboarding

Whether building trading assistants, portfolio managers, or DeFi automation tools, Rig Onchain Kit abstracts blockchain complexity while maintaining full control over transaction security and model behavior. The included HTTP service layer enables seamless integration with web frontends while the modular architecture allows incremental adoption of specific components.

Installation

In order to quickly get set-up, you can add Rig Onchain Kit to your project with

cargo add rig-onchain-kit --features full

Note that full contains both Solana, EVM and http features, where Solana and EVM are available separately

cargo add rig-onchain-kit --features solana  # less dependencies

If you need to add custom tools, with the #[tool] macro, be sure to

cargo add rig-tool-macro

In case you are running in a remote environment, should you run into SSL errors - be sure to include the TLS deps, with

sudo apt-get update && sudo apt-get install -y \
    ca-certificates \
    openssl \
    libssl3

Configuration

Rig-onchain-kit uses below environment variables, each is corresponding to given features, e.g. --features solana is going to require the # solana env vars set.

ANTHROPIC_API_KEY=""

# solana
SOLANA_PRIVATE_KEY=""
SOLANA_RPC_URL=""

# evm
ETHEREUM_PRIVATE_KEY=""
ETHEREUM_RPC_URL=""

# http
PRIVY_APP_ID=""
PRIVY_APP_SECRET=""
PRIVY_VERIFICATION_KEY=""

In case the http feature is used, the private keys are managed by Privy, making the SOLANA_PRIVATE_KEY and ETHEREUM_PRIVATE_KEY no longer required.

The default agents are using Claude under the hood, which maintains the balance of speed and accuracy, other models might be supported in the future but currently, Claude is best-in-class

Quick Start

First, ensure the installation and the configuration steps are completed

Import an agent of choice, along with the SignerContext and the local signer struct

use std::sync::Arc;
use rig_onchain_kit::agent::create_solana_agent;
use rig_onchain_kit::signer::SignerContext;
use rig_onchain_kit::signer::solana::LocalSolanaSigner,
use rig::completion::Prompt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let private_key = std::env::var("SOLANA_PRIVATE_KEY")?;

    let signer = LocalSolanaSigner::new(private_key);

    SignerContext::with_signer(Arc::new(signer), async {
        let agent = create_solana_agent();
        let response = agent.prompt("what is my public key?")?);
        println!("{}", response);
    });

    Ok(())
}

For more examples, check out the examples directory, you can run each with cargo run --example [name]

Tools

Any function can be transformed into a tool by applying a #[tool] macro onto it

#![allow(unused)]
fn main() {
#[tool] // <- this is the macro
fn add(a: i32, b: i32) -> i32 {
    a + b
}
}

Then, when creating the agent, in order to supply the agent with the tool

#![allow(unused)]
fn main() {
let agent = rig::providers::anthropic::Client::from_env()
    .agent(rig::providers::anthropic::CLAUDE_3_5_SONNET)
    .preamble("you are a friendly calculator")
    .max_tokens(1024)
    .tool(Add) // tool becomes present thanks to the macro
    .build();
}

After that, the model is able to perform actions!

Built-in tools

ℹ️ To see all of the currently available tools, you can check Solana and EVM toolsets

Throughout rig-agent-kit follows an opinionated way of implementing tools:

  1. critical: ensuring that tools are called inside of the SignerContext block - this allows to identify the transaction signer - the owner, exposed into the closure
  2. creating a transaction for a given action
  3. executing the transaction with the TransactionSigner contained by the SignerContext (more on this in the next chapter)
  4. All of the tools return a Result<T>

Wrapping the signers is tricky, so rig-onchain-kit comes with helpers, both for EVM and Solana, arriving at this concise end-result

#![allow(unused)]
fn main() {
#[tool]
pub async fn transfer_sol(to: String, amount: u64) -> Result<String> {
    execute_solana_transaction(move |owner| async move {
        create_transfer_sol_tx(&Pubkey::from_str(&to)?, amount, &owner).await
    })
    .await
}
}

This design allows to use different transaction signing and sending methods and ensuring highly concurrent services using rig-onchain-kit work well

Custom tools

In order to implement extra tools, you can import the helpers along with

#![allow(unused)]
fn main() {
use rig_tool_macro::tool;
use rig_agent_kit::solana::execute_solana_transaction;

use crate::your_package::create_your_custom_tx;

#[tool]
pub async fn custom_tool() -> Result<String> {
    execute_solana_transaction(move |owner| async move {
       // note: the `owner` address/pubkey is available as `String` to consume
       create_your_custom_tx(&owner).await
    })
    .await
}
}

⚠️ The tool macro acccepts only the native JSON types, like string, bool, number etc, structs and nested types are not supported, so neither a Pubkey and an Address are not allowed, those have to be parsed before passing to the corresponding transaction creators

SignerContext

The SignerContext is a crucial building block of the rig-onchain-kit - it allows to restrict the scope of the signer private key into a thread-local variable

It allows multi-tenancy, where any user chatting with the AI model is able to maintain the context of their account and their account only in a non-locking manner

SignerContext exposes a ::with_signer() method, which takes a TransactionSigner trait

It can be used like this

#![allow(unused)]
fn main() {
fn example() {
    SignerContext::with_signer(Arc::new(signer), async {
        // any tool calls inside of this block have the signer passed on
    });
}
}

rig-onchain-kit currently supports local signers (the LocalSolanaSigner, LocalEvmSigner) as well as the PrivySigner for remote signatures

The core methods of the TransactionSigner can be implemented with any KMS, allowing integrations with keys stored inside of HashiCorp Vault, AWS KMS etc, as well as providers for smart transactions, like Helius or other wallet management providers, say Magic

High-level interface is as per below snippet

#![allow(unused)]
fn main() {
#[async_trait]
pub trait TransactionSigner: Send + Sync {
    #[cfg(feature = "solana")]
    fn pubkey(&self) -> String;

    #[cfg(feature = "solana")
    async fn sign_and_send_solana_transaction(
        &self,
        _tx: &mut solana_sdk::transaction::Transaction,
    ) -> Result<String>;

    #[cfg(feature = "evm")]
    fn address(&self) -> String;

    #[cfg(feature = "evm")]
    async fn sign_and_send_evm_transaction(
        &self,
        _tx: alloy::rpc::types::TransactionRequest,
    ) -> Result<String>;
}
}

In summary, SignerContext combined with TransactionSigner provide the required level of abstraction, where remote keys are safely stored and each request is processed in its corresponding scope, making rig-onchain-kit a scalable solution

For a production-style implementation with Privy, check out the src/http/routes.rs stream endpoint

Solana

Key Features

  • Token Operations

    • Jupiter swap integration for token trading
    • SPL token transfers and balance checks
    • Token deployment capabilities
    • Price fetching and portfolio management
  • Basic Operations

    • SOL transfers
    • Balance queries
    • Public key management
    • Portfolio tracking
  • PumpFun Token Features

    • Token deployment with customizable parameters
    • Buy/sell functionality
    • Price discovery through DexScreener

Main Tools

The module exposes several key tools:

perform_jupiter_swap()    // Execute token swaps via Jupiter
transfer_sol()            // Send SOL to another address
transfer_spl_token()      // Transfer SPL tokens
get_public_key()          // Retrieve signer's public key
get_sol_balance()         // Check SOL balance
get_spl_token_balance()   // Check SPL token balance
deploy_pump_fun_token()   // Deploy on pump.fun
fetch_token_price()       // Get current token prices
get_portfolio()           // Retrieve full portfolio details
search_on_dex_screener()  // search for a ticker/mint

Configuration

The module requires a Solana RPC URL which can be set via the SOLANA_RPC_URL environment variable. If not specified, it defaults to the public Solana mainnet RPC endpoint.

EVM

Key Features

  • Token Operations

    • Uniswap integration for token swaps
    • ERC20 token transfers and allowance management
    • Token balance checks
    • Router approval verification
  • Basic Operations

    • ETH transfers
    • Balance queries
    • Wallet address management
    • Gas estimation and transaction handling

Main Tools

The module exposes several key tools:

#![allow(unused)]
fn main() {
verify_swap_router_has_allowance()  // Check DEX trading permissions
approve_token_for_router_spend()    // Approve tokens for trading
trade()                             // Execute token swaps via Uniswap
transfer_eth()                      // Send ETH to another address
transfer_erc20()                    // Transfer ERC20 tokens
wallet_address()                    // Get current wallet address
get_eth_balance()                   // Check ETH balance
get_erc20_balance()                 // Check ERC20 token balance
}

Configuration

The module requires an Ethereum RPC URL which can be set via the ETHEREUM_RPC_URL environment variable. It supports multiple EVM-compatible chains through provider configuration.

Cross-chain

Key Features

  • Cross-chain Swaps & Bridges

    • Seamless token swaps between Solana and EVM chains
    • Bridge functionality for moving assets across chains
    • Quote system for cost estimation
    • Support for both native and wrapped tokens
  • Token Operations

    • ERC20 approval management for cross-chain bridges
    • Token allowance verification
    • Automatic address resolution based on chain type

Main Tools

The module exposes several key tools for cross-chain operations:

#![allow(unused)]
fn main() {
get_multichain_quote()  // Get cost estimate for cross-chain swaps
multichain_swap()       // Execute cross-chain token swaps/bridges
check_approval()        // Verify ERC20 token approvals
approve_token()         // Approve ERC20 tokens for bridge contracts
}

Configuration

The module requires an Ethereum RPC URL which can be set via the ETHEREUM_RPC_URL environment variable. It supports multiple EVM-compatible chains through provider configuration.

Important Notes

  1. Token Decimals: Amount parameters must account for token decimals:

    • USDC: 6 decimals (1 USDC = 1000000)
    • SOL: 9 decimals (1 SOL = 1000000000)
    • ETH: 18 decimals (1 ETH = 1000000000000000000)
  2. Gas Requirements: Users must have native tokens (SOL/ETH) on both chains to cover gas fees, unless using sponsored transactions (coming soon).

  3. Token Identification: Tokens can be specified using:

    • Symbol (e.g., "USDC")
    • Solana public key
    • EVM contract address
  4. Chain Support: Currently running Solana and Arbitrum, EVM is not fully multi-tenant, need some tweaks. Full coverage on the way

HTTP Service

The rig-onchain-kit provides a production-ready HTTP service that enables:

  • Server-Sent Events (SSE) streaming for real-time AI agent responses
  • Multi-chain support (EVM and Solana) through feature flags
  • User authentication and wallet management via Privy
  • Concurrent handling of multiple chat sessions

Core Endpoints

The service exposes these main endpoints:

POST /v1/stream   - Stream AI agent responses
GET  /v1/auth     - Verify authentication status
GET  /healthz     - Health check endpoint

Streaming Endpoint

The /v1/stream endpoint accepts:

{
  prompt: string,
  chat_history: Message[],
  chain: "solana" | "evm" | "pump" // Chain selection
}

It returns a Server-Sent Events stream containing:

{
  type: "Message" | "ToolCall" | "Error",
  content: {
    // For Message: string with AI response
    // For ToolCall: { name: string, result: string }
    // For Error: error message string
  }
}

Features

Chain Selection

The service supports multiple blockchain environments through feature flags:

#![allow(unused)]
fn main() {
// Select agent based on chain parameter
match request.chain.as_deref() {
    #[cfg(feature = "solana")]
    Some("solana") => state.solana_agent.clone(),
    #[cfg(feature = "evm")]
    Some("evm") => state.evm_agent.clone(),
    // ...
}
}

Concurrent Sessions

The service handles multiple simultaneous chat sessions using Tokio channels and tasks:

#![allow(unused)]
fn main() {
let (tx, rx) = tokio::sync::mpsc::channel::<sse::Event>(32);
spawn_with_signer(signer, || async move {
    // Handle individual chat session
}).await;
}

Keep-alive & Retry Logic

The SSE implementation includes built-in keep-alive and retry mechanisms:

#![allow(unused)]
fn main() {
sse::Sse::from_infallible_receiver(rx)
    .with_keep_alive(Duration::from_secs(15))
    .with_retry_duration(Duration::from_secs(10))
}

Configuration

The service is configured through the AppState which manages:

  • Chain-specific AI agents
  • Wallet manager instance
  • Authentication settings
#![allow(unused)]
fn main() {
let state = AppState::builder()
    .with_wallet_manager(wallet_manager)
    .with_solana_agent(solana_agent)    // Optional
    .with_evm_agent(evm_agent)          // Optional
    .build()?;
}

Why and how?

While having a possibility of creating local agents is nice, it is all about scale

In order to provide AI Agent powered interactions for the users, it is crucial to be able to expose the agents built with rig-onchain-kit as a service, consumable by a frontend

Luckily, the framework comes pre-packaged with a production-ready service for maintaining thousands of simulatenous conversations, each completely encapsulated

To provide a backend for multi-VM platform, with end user wallet management for both EVM and Solana, you can

git clone https://github.com/piotrostr/listen
cd listen/listen-kit
cp .env.example .env
vi .env # fill in the env vars, include the PRIVY_* variables
cargo run --bin server --features full

To find the configuration variables, you can go to your Privy dashboard

ℹ️ the PRIVY_VERIFICATION_KEY has to come in the format of "-----BEGIN PUBLIC KEY-----\n<secret>\n-----END PUBLIC KEY-----" note how there are '\n' newline separators, entire secret in a single line

Next chapter outlines how to authenticate your users.

Authentication

After running cargo run --bin server --features full, you can make requests to the backend from a frontend that implements Privy authentication

The service exposed by rig-onchain-kit with the http feature uses Privy for frontend to backend authentication

⚠️ Be sure to include the same PRIVY_APP_ID both in the backend and frontend configuration

On your frontend, Privy can be set up as per the documentation here

The backend expects the user to have created an embedded wallet and have delegated access to the application, for both the EVM and Solana embedded wallets

After completing the authentication, you can use the getAuthToken method from the Privy SDK to authenticate a given user on the backend

Frontend (React example)

import { usePrivy } from "@privy-io/react-auth";
import { useCallback } from "react";

function Chat() {
  const { getAccessToken } = usePrivy();

  const sendMessage = useCallback(async (userMessage: string) => {
    const body = JSON.stringify({
      prompt: userMessage,
      chat_history: chat_history,
      chain: chatType,
    });

    // post the request to the `rig-onchain-kit` service
    const response = await fetch(config.API_BASE_URL + "/v1/stream", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + (await getAccessToken()),
      },
      body,
    });
  });
}

Full example

On the backend, the token is picked up by middleware and passed onto the the Privy WalletManager implementation, which parses the JWT token sent from the frontend

#![allow(unused)]
fn main() {
pub fn validate_access_token(
    &self,
    access_token: &str,
) -> Result<PrivyClaims> {
    let mut validation = Validation::new(Algorithm::ES256);
    validation.set_issuer(&["privy.io"]);
    validation.set_audience(&[self.privy_config.app_id.clone()]);

    let key = DecodingKey::from_ec_pem(
        self.privy_config.verification_key.as_bytes(),
    )?;

    let token_data =
        decode::<PrivyClaims>(access_token, &key, &validation)
            .map_err(|_| anyhow!("Failed to authenticate"))?;

    Ok(token_data.claims)
}
}

Afterwards, the given user profile is fetched to find the wallets and construct the UserSession, that is later used for finding the signing address to use

This prevents unauthorized access as well as allows to bind the request to a given user