Tool Oracle

On-chain LLM inference calls. The Tool Oracle lets you make LLM inference calls with on-chain inputs and configuration, regex-filter outputs, and write results back on-chain where your programs can consume them.

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:

721QWDeUzVL77UCzCFHsVGCMBVup8GsAMPaD2YvWvw97

In 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 Oracle
  • anchor-client: high-level client for sending instructions
  • solana-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_types

You 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 bytes
  • ToolOracleRequestInput::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 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;

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-client and deriving the request PDA
  • Choosing escrow, max_requests, and an output_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.