<aside>
:ambient-logo-original: Docs Home
🌐 Homepage
:discord-symbol-blurple: Discord
:github-mark-white-bg: github.com/ambient-xyz
:x-logo-black: @ambient_xyz
:x-logo-black: @IridiumEagle
</aside>
The Ambient Tool Oracle is a combination of an on-chain program and an off-chain service that lets you 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 they can be consumed by your own programs.
This tutorial focuses on using the Tool Oracle program itself. The crypto ticker CLI in this repo is just one minimal example client that you can use as a reference or starting point.
The current Tool Oracle program ID is: 721QWDeUzVL77UCzCFHsVGCMBVup8GsAMPaD2YvWvw97
In code, you typically depend on oracle-program-types, which exposes this as oracle_program_types::ID.
References:
To talk to the Tool Oracle from Rust, you’ll usually use:
oracle-program-types – type-safe accounts, instructions, and enums for the Tool Oracle.anchor-client – high-level client for sending instructions.solana-sdk, solana-client, solana-account-decoder-client-types – Solana primitives and RPC/WebSocket clients.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:
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_types
You can optionally create a PubsubClient if you want to subscribe to account changes instead of polling.
The request the oracle processes is described by ToolOracleRequestAccount. It contains:
initial_prompt).max_requests).output_filter).state).There are two ways to supply the prompt:
ToolOracleRequestInput::Direct(prompt) – inline string input, capped at 800 bytes.ToolOracleRequestInput::Account(pubkey) – 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(),
);
You create a ToolOracleRequestAccount that encodes your intent:
let request_data = ToolOracleRequestAccount {
state: ToolOracleRequestState::Requested,
initial_prompt,
max_requests: 5,
output_filter: Some("^[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,
);
Note: Each payer may only have one in‑progress Tool Oracle request at a time. If a request is still in Requested or Started state, attempting to allocate a new one for the same payer will fail.
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 request.max_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;
Warning: If escrow is depleted below the amount required to perform further operations, the oracle marks the request as failed. Check the ToolOracleRequestState::Failed variant for the reason and treat the request as terminal.
The escrow is debited from the payer in addition to rent required to keep the accounts alive.
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.
The ToolOracleRequestAccount progresses through several states:
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.There are two common ways to get the result:
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;
}
}
}
The crypto ticker example uses a WebSocket subscription so it can 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;
Once the state is Completed, read the output field:
AccountOrString::Direct(text) – response is inlined in the account.