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:
- critical: ensuring that tools are called inside of the
SignerContext
block - this allows to identify the transaction signer - the owner, exposed into the closure - creating a transaction for a given action
- executing the transaction with the
TransactionSigner
contained by theSignerContext
(more on this in the next chapter) - 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 aPubkey
and anAddress
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
-
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)
-
Gas Requirements: Users must have native tokens (SOL/ETH) on both chains to cover gas fees, unless using sponsored transactions (coming soon).
-
Token Identification: Tokens can be specified using:
- Symbol (e.g., "USDC")
- Solana public key
- EVM contract address
-
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,
});
});
}
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