The Ambient Tool Oracle pairs an on-chain program with an off-chain service so you can make multiple LLM inference calls using inputs and configuration provided from an on-chain source. Outputs can be filtered with regex and written back on-chain, where your own programs can consume them.
This page covers the Tool Oracle program itself. The crypto ticker CLI is one minimal example client. Use it as a reference or a starting point, not as the boundary of what the oracle can do.
The current Tool Oracle program ID is:
721QWDeUzVL77UCzCFHsVGCMBVup8GsAMPaD2YvWvw97In code, you typically depend on oracle-program-types, which exposes this
as oracle_program_types::ID.
Reference implementation: oracle-crypto-ticker-app.
Request lifecycle#
A ToolOracleRequestAccount progresses through several states:
| State | Meaning |
|---|---|
Requested | Request recorded on-chain, waiting for a worker |
Started { worker, .. } | A worker has claimed the request |
Completed { output, .. } | The oracle has finished and produced output |
Failed { reason, completed_requests_arr } | The request encountered an error |
Completed and Failed are terminal. The steps below create a request,
follow it to one of those states, and then reclaim the accounts.
1. Dependencies#
To talk to the Tool Oracle from Rust, you'll usually use:
oracle-program-types: type-safe accounts, instructions, and enums for the Tool Oracleanchor-client: high-level client for sending instructionssolana-sdk,solana-client,solana-account-decoder-client-types: Solana primitives and RPC/WebSocket clients- An async runtime such as
tokio
The example CLI also uses clap, tracing, anyhow, and serde_json, but
those are not required to integrate with the Tool Oracle itself.
You will need:
- A Solana keypair with SOL to pay for rent, fees, and escrow
- RPC and (optionally) WebSocket endpoints for your target cluster
2. Set up clients#
First, construct a Solana client that can talk to the Tool Oracle program.
Using anchor-client:
let cluster = Cluster::Custom(rpc_url, ws_url);
let payer = Arc::new(read_keypair_file(payer_path)?);
let program_client = anchor_client::Client::new_with_options(
cluster,
payer.clone(),
CommitmentConfig::processed(),
);
let program = program_client.program(ID)?; // ID from oracle_program_typesYou can optionally create a PubsubClient if you want to subscribe to
account changes instead of polling.
3. Craft a request#
The request the oracle processes is described by ToolOracleRequestAccount.
It contains:
- The initial prompt or tool description (
initial_prompt) - Limits on how many external inference calls may be made (
max_requests) - An optional regex filter for the final output (
output_filter) - The current request state (
state)
Input prompt#
There are two ways to supply the prompt:
ToolOracleRequestInput::Direct(prompt): inline string input, capped at 800 bytesToolOracleRequestInput::Account(pubkey): the prompt is read from another account and interpreted as UTF-8
Most simple prompts can use the direct variant:
let initial_prompt = ToolOracleRequestInput::Direct(
"Describe the latest price action for SOL in one sentence.".to_string(),
);Request account layout#
You create a ToolOracleRequestAccount that encodes your intent:
let request_data = ToolOracleRequestAccount {
state: ToolOracleRequestState::Requested,
initial_prompt,
max_requests: 5,
output_filter: Some(r"^[0-9]+(\.[0-9]{2})?$".to_string()),
};The request account itself is a PDA derived from a fixed seed and the payer's pubkey:
let (request_account, _) = Pubkey::find_program_address(
&[b"tool-oracle-request", payer.pubkey().as_ref()],
&ID,
);4. Escrow and limits#
Every request holds some lamports in escrow to pay for LLM inference and the off-chain service:
escrow: how many lamports you are willing to spend on this requestmax_requests: how many external inference calls the oracle is allowed to perform on your behalf
Both are configured per request, so you can tune cost and behavior on a per-use basis.
let escrow: u64 = 1_000_000; // example
let max_requests: u8 = 5;The escrow is debited from the payer in addition to rent required to keep the accounts alive.
5. Submit a request#
To create the request account and start processing, you send a
CreateRequest instruction. With anchor-client:
let output_account = None; // or Some(pubkey) if you want output in a separate account
let sig = program
.request()
.accounts(program_accounts::CreateRequest {
new_account: request_account,
signer: payer.pubkey(),
system_program: system_program::id(),
output_account,
})
.args(program_args::CreateRequest {
output_account_size: None,
request: request_data,
escrow,
})
.signer(payer.clone())
.options(CommitmentConfig::processed())
.send()
.await?;At this point, the request is on-chain, and the off-chain workers can pick it up and start making tool / LLM calls as described by your input.
6. Track request state and get the result#
There are two common ways to get the result.
A. Polling (simplest)#
You can poll the request account until it reaches a terminal state:
loop {
let acct = program
.account::<ToolOracleRequestAccount>(request_account)
.await?;
match acct.state {
ToolOracleRequestState::Completed { output, .. } => {
// handle output (see below)
break;
}
ToolOracleRequestState::Failed { reason, .. } => {
// handle error and break
break;
}
_ => {
// still in progress; sleep briefly and try again
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}B. PubSub subscription#
Used by the crypto ticker example. A WebSocket subscription lets you react to changes as soon as they land on-chain:
let (mut updates, unsub) = pubsub
.account_subscribe(&request_account, Some(config))
.await?;
while let Some(acct) = updates.next().await {
let decoded: ToolOracleRequestAccount = /* decode account data */;
match decoded.state {
ToolOracleRequestState::Completed { output, .. } => {
// handle output (see below)
break;
}
ToolOracleRequestState::Failed { reason, .. } => {
// handle error and break
break;
}
_ => {}
}
}
unsub().await;Interpreting the output#
Once the state is Completed, read the output field. It is an
AccountOrString with two variants:
| Variant | Where the response lives |
|---|---|
AccountOrString::Direct(text) | Inlined in the request account |
AccountOrString::Account(pubkey) | Stored in another account. Fetch it with program.rpc().get_account(pubkey) and interpret as UTF-8 |
From there, parse or post-process the string as needed (JSON decoding, numeric parsing) and feed it into your own program or off-chain logic.
7. Clean up accounts#
Because only one request may be in-flight per payer, clean up old request accounts once you are done with them.
The oracle exposes a ReclaimAccounts instruction to close the request (and
optional output) account and return the remaining lamports to a destination:
program
.request()
.accounts(program_accounts::ReclaimAccounts {
job_request: request_account,
output: output_account,
destination: payer.pubkey(),
signer: payer.pubkey(),
system_program: system_program::id(),
})
.args(program_args::ReclaimAccounts {
destination: payer.pubkey(),
})
.signer(payer.clone())
.send()
.await?;8. Using the crypto ticker example#
The CLI in oracle-crypto-ticker-app ties all of the above together into a single example: it builds a prompt asking for the current USD price of a chosen ticker symbol, submits a Tool Oracle request, waits for completion, and prints the result.
Use it as a reference implementation for:
- Setting up
anchor-clientand deriving the request PDA - Choosing
escrow,max_requests, and anoutput_filter - Subscribing to account updates to react to state changes in real time
- Cleaning up request and output accounts when finished
Once you are comfortable with the example, adapt the same pattern to your own contracts and clients: richer prompts, tool-based workflows, or program-controlled follow-up actions driven by the oracle's output.
Related pages#
- The auction program: how inference requests are bundled, auctioned, and verified on-chain
- Solana quickstart: Ambient's Solana-compatible RPC
- Verified inference: what the network guarantees about the output the oracle writes back