<aside>

Overview

:ambient-logo-original: Docs Home

What is Ambient?

Getting Started

Devnet

App

API

On-Chain

Tool Oracle

Miners

Encyclopedia

Solana Quickstart

Developers

API Docs

API Playground

Links

🌐 Homepage

✉️ [email protected]

:discord-symbol-blurple: Discord

:github-mark-white-bg: github.com/ambient-xyz

:x-logo-black: @ambient_xyz

:x-logo-black: @IridiumEagle

📄 Litepaper

</aside>

Using the Ambient Tool Oracle

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:


1. Dependencies

To talk to the Tool Oracle from Rust, you’ll usually use:

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:


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:

Input prompt

There are two ways to supply the prompt:

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("^[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.


4. Escrow and Limits

Every request holds some lamports in escrow to pay for LLM inference and the off-chain service.

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.


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

The ToolOracleRequestAccount progresses through several states:

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 this example)

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;

Interpreting the output

Once the state is Completed, read the output field: