Chio/Docs
LOGIN · JOIN

Pricing as Native Tools

Turn Larkfield's quote and refund functions into agent tools, run them through the kernel, bound their use, and keep the signed record.

Guide chio@5f63e6e

On this page

A Rust function becomes an agent's capability in three moves: describe it as a tool, register it with the kernel, and grant an agent the right to call it. Registering a tool grants nothing; the grant decides which registered tool an agent may use, and how often.

Starts from chio new pricing-tools --example pricing-tools. The project serves quote and refund on the pricing server, grants an agent two quotes, and then shows the two refusals that matter: a refund the grant never covered, and a third quote past the limit. It runs locally with no model API key.

Start

Give an agent a useful operation

Your code defines what quote and refund do; the kernel governs who may invoke them. The project holds the service, an embedded kernel host, the grant, SQLite receipt storage, and an offline verifier.

1. Run the tools through the kernel

You need Rust; the toolchain file and lockfile select the compiler and the Chio crates. You can also download the project. The first build compiles the dependency graph.

Terminalbash
chio new pricing-tools --example pricing-tools && cd pricing-tools
cargo run --release --locked
Output
call-1: ALLOW {"currency":"USD","percent":10.0,"price":"21.60","sku":"sku-118"}
call-2: ALLOW {"currency":"USD","percent":25.0,"price":"43.50","sku":"sku-204"}
outside-grant: DENY requested tool refund on server pricing is not in capability scope
exhausted-limit: DENY invocation budget exhausted for capability cap-01a12eff-a57c-76f4-941e-55509841be30
evidence: runs/91c940d4-9111-411e-9d85-cdefe526aa69

You should see two quotes, USD 21.60 and USD 43.50, then the refund refused because the grant covers only quote, then a third quote refused because the grant allows two. Receipt and run IDs change each time.

Separate the operation from its authority

The service owns the handlers

This is the complete src/service.rs. Each tool validates its arguments in the handler and returns JSON. NativeChioServiceBuilder binds both to the server identity pricing.

src/service.rs · completerust
use chio_kernel::KernelError;
use chio_mcp_adapter::native::{NativeChioService, NativeChioServiceBuilder, NativeTool};
use serde_json::{json, Value};

/// Larkfield list prices in cents, as main holds them after the Quickstart.
const PRICES: [(&str, i64); 3] = [("sku-118", 2400), ("sku-204", 5800), ("sku-301", 14000)];
/// Larkfield's one open order: its SKU and the amount paid, in cents.
const ORDERS: [(&str, &str, i64); 1] = [("order-77", "sku-204", 5800)];

pub fn build(public_key: String) -> anyhow::Result<NativeChioService> {
    Ok(NativeChioServiceBuilder::new("pricing", public_key)
        .server_name("Larkfield pricing")
        .server_version("1.0.0")
        .tool(
            NativeTool::new(
                "quote",
                "Quote a SKU at a discount, rounded half up to the cent",
                json!({
                    "type": "object",
                    "properties": {"sku": {"type": "string"}, "percent": {"type": "number"}},
                    "required": ["sku", "percent"]
                }),
            )
            .read_only(),
            |arguments| {
                let sku = text(&arguments, "sku")?;
                let percent = arguments
                    .get("percent")
                    .and_then(Value::as_f64)
                    .filter(|percent| (0.0..=100.0).contains(percent))
                    .ok_or_else(|| incomplete("percent must be between 0 and 100"))?;
                let cents = PRICES
                    .iter()
                    .find(|(id, _)| *id == sku)
                    .map(|(_, cents)| *cents)
                    .ok_or_else(|| incomplete("unknown sku"))?;
                // Basis points keep the arithmetic exact; the last step rounds half up.
                let basis = (percent * 100.0).round() as i64;
                let price = (cents * (10_000 - basis) + 5_000) / 10_000;
                Ok(json!({"sku": sku, "percent": percent, "price": usd(price), "currency": "USD"}))
            },
        )
        .tool(
            NativeTool::new(
                "refund",
                "Refund an order in full",
                json!({
                    "type": "object",
                    "properties": {"order": {"type": "string"}},
                    "required": ["order"]
                }),
            ),
            |arguments| {
                let order = text(&arguments, "order")?;
                let (id, _, paid) = ORDERS
                    .iter()
                    .find(|(id, _, _)| *id == order)
                    .ok_or_else(|| incomplete("unknown order"))?;
                Ok(json!({"order": id, "refunded": usd(*paid), "currency": "USD"}))
            },
        )
        .build()?)
}

fn text<'a>(arguments: &'a Value, field: &str) -> Result<&'a str, KernelError> {
    arguments
        .get(field)
        .and_then(Value::as_str)
        .ok_or_else(|| incomplete(&format!("{field} must be a string")))
}

fn incomplete(message: &str) -> KernelError {
    KernelError::RequestIncomplete(message.into())
}

fn usd(cents: i64) -> String {
    format!("{}.{:02}", cents / 100, cents % 100)
}

The input schema describes the interface; the handler enforces it. quote carries the read_only annotation in its manifest, and refund does not, because a refund moves money.

The host grants permission

Registration makes both handlers available to the kernel. This separate step in src/host.rs grants the agent only one of them, for five minutes:

src/host.rs · in bootrust
    let capability = kernel.issue_capability(
        &agent.public_key(),
        ChioScope {
            grants: vec![ToolGrant {
                server_id: "pricing".into(),
                tool_name: ALLOWED_TOOL.into(),
                operations: vec![Operation::Invoke],
                constraints: Vec::new(),
                max_invocations: Some(CALL_LIMIT),
                max_cost_per_invocation: None,
                max_total_cost: None,
                dpop_required: None,
            }],
            ..ChioScope::default()
        },
        300,
    )?;

Change the two constants at the top of the same file to allow one refund instead, then run again:

src/host.rs · constantsrust
pub const ALLOWED_TOOL: &str = "refund";
pub const CALL_LIMIT: u32 = 1;
Output
call-1: ALLOW {"currency":"USD","order":"order-77","refunded":"58.00"}
outside-grant: DENY requested tool quote on server pricing is not in capability scope
exhausted-limit: DENY invocation budget exhausted for capability cap-01a12eff-9387-71a9-8112-1804a5d685fc
evidence: runs/744d6779-509d-4566-a007-d0518cbc158f

Now the refund of order-77 returns USD 58.00, quote is outside the grant, and a second refund is refused: the money moved once. Each process gets a new allowance; this host keeps no lifetime quota across restarts.

Authenticate the service definition

The host signs the manifest with the service key, verifies that signature, saves the signed manifest, and registers the service. The manifest authenticates the tool descriptions; the grant above authorizes their use.

src/host.rs · sign, verify, registerrust
    let service_key = Keypair::generate();
    let service = crate::service::build(service_key.public_key().to_hex())?;
    let manifest = chio_manifest::sign_manifest(service.manifest(), &service_key)?;
    chio_manifest::verify_manifest(&manifest, &service_key.public_key())?;
    std::fs::write(
        directory.join("manifest.json"),
        serde_json::to_string_pretty(&manifest)?,
    )?;
    kernel.register_tool_server(Box::new(service));

Send each request through the kernel

The application sends every request with a fresh request ID and the issued capability. This function in src/main.rs is the mediation point; calling the service's invoke directly would skip it.

src/main.rs · complete call functionrust
async fn call(
    kernel: &ChioKernel,
    capability: &CapabilityToken,
    tool: &str,
    arguments: Value,
) -> anyhow::Result<ToolCallResponse> {
    Ok(kernel
        .evaluate_tool_call(&ToolCallRequest {
            request_id: Uuid::new_v4().to_string(),
            capability: capability.clone(),
            tool_name: tool.into(),
            server_id: "pricing".into(),
            agent_id: capability.subject.to_hex(),
            arguments,
            dpop_proof: None,
            execution_nonce: None,
            governed_intent: None,
            approval_token: None,
            approval_tokens: Vec::new(),
            threshold_approval_proposal: None,
            supplemental_authorization: None,
            model_metadata: None,
            federated_origin_kernel_id: None,
        })
        .await?)
}
Complete host initialization

The host installs SQLite before it issues authority. Signing keys, revocation state, and invocation accounting belong to this process; the JSON and SQLite files keep its records.

src/host.rs · completerust
use chio_core::{
    capability::{
        scope::{ChioScope, Operation, ToolGrant},
        token::CapabilityToken,
    },
    crypto::Keypair,
};
use chio_kernel::{
    ChioKernel, KernelConfig, DEFAULT_CHECKPOINT_BATCH_SIZE, DEFAULT_MAX_STREAM_DURATION_SECS,
    DEFAULT_MAX_STREAM_TOTAL_BYTES,
};

// Change these to choose which registered operation the agent can invoke.
pub const ALLOWED_TOOL: &str = "quote";
pub const CALL_LIMIT: u32 = 2;

pub struct Host {
    pub kernel: ChioKernel,
    pub capability: CapabilityToken,
}

pub fn boot(directory: &std::path::Path) -> anyhow::Result<Host> {
    let kernel_key = Keypair::generate();
    // This is the host's chosen key, saved before receiving any receipt.
    // A remote verifier must obtain its expected key through a trusted channel.
    std::fs::write(
        directory.join("kernel-public-key.txt"),
        format!("{}\n", kernel_key.public_key().to_hex()),
    )?;
    let mut kernel = ChioKernel::new(KernelConfig {
        keypair: kernel_key.clone(),
        ca_public_keys: Vec::new(),
        max_delegation_depth: 3,
        policy_hash: chio_core::sha256_hex(include_bytes!("host.rs")),
        allow_sampling: false,
        allow_sampling_tool_use: false,
        allow_elicitation: false,
        max_stream_duration_secs: DEFAULT_MAX_STREAM_DURATION_SECS,
        max_stream_total_bytes: DEFAULT_MAX_STREAM_TOTAL_BYTES,
        require_web3_evidence: false,
        // Keys and budget counters live for one process. A SQLite store
        // retains capabilities and receipts for this run.
        allow_ephemeral_receipt_log: true,
        allow_ephemeral_revocation_store: true,
        checkpoint_batch_size: DEFAULT_CHECKPOINT_BATCH_SIZE,
        retention_config: None,
        memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
        deadlines: chio_kernel::HotPathDeadlineConfig::default(),
    });
    // Install storage before issuance, then retain the run's signed receipts.
    kernel.set_receipt_store(Box::new(chio_store_sqlite::SqliteReceiptStore::open(
        directory.join("receipts.db"),
    )?))?;
    let service_key = Keypair::generate();
    let service = crate::service::build(service_key.public_key().to_hex())?;
    let manifest = chio_manifest::sign_manifest(service.manifest(), &service_key)?;
    chio_manifest::verify_manifest(&manifest, &service_key.public_key())?;
    std::fs::write(
        directory.join("manifest.json"),
        serde_json::to_string_pretty(&manifest)?,
    )?;
    kernel.register_tool_server(Box::new(service));
    let agent = Keypair::generate();
    let capability = kernel.issue_capability(
        &agent.public_key(),
        ChioScope {
            grants: vec![ToolGrant {
                server_id: "pricing".into(),
                tool_name: ALLOWED_TOOL.into(),
                operations: vec![Operation::Invoke],
                constraints: Vec::new(),
                max_invocations: Some(CALL_LIMIT),
                max_cost_per_invocation: None,
                max_total_cost: None,
                dpop_required: None,
            }],
            ..ChioScope::default()
        },
        300,
    )?;
    Ok(Host { kernel, capability })
}

Read the four decisions

RequestResultWhat it establishes
quote, calls 1 and 2
ALLOW, USD 21.60 and USD 43.50
The handler ran under the grant.
refund for order-77
DENY, outside scope
Registering a tool did not grant access to it.
quote, call 3
DENY, allowance exhausted
The process spent the two authorized calls.

Keep and verify the signed records

The printed run directory holds the signed manifest, the capability, the kernel's public key, receipts.db, and the four receipts; the program checked each signature before saving it.

The run directorytext
call-1-receipt.json
call-2-receipt.json
capability.json
exhausted-limit-receipt.json
kernel-public-key.txt
manifest.json
outside-grant-receipt.json
receipts.db

Verify the refused refund against the key the host wrote before it issued any authority:

Terminal · pricing-toolsbash
cd runs/91c940d4-9111-411e-9d85-cdefe526aa69
cargo run --release --locked --bin verify-receipt -- \
  outside-grant-receipt.json kernel-public-key.txt
Output
{
  "authorized": false,
  "crypto_error": null,
  "crypto_floor": "allow_classical",
  "decision": {
    "guard": "kernel",
    "reason": "requested tool refund on server pricing is not in capability scope",
    "verdict": "deny"
  },
  "ok": true,
  "parameter_hash_valid": true,
  "receipt_id": "0c8dc3e4b6ebe95b7cca1d994c77dbd62e293e9cb1f091a6ac3a5a82eb00b020",
  "receipt_id_valid": true,
  "signature_valid": true,
  "signer_trusted": true
}

You should see ok: true with authorized: false: an authentic record of a refusal. The receipt names the refused tool, its arguments, and the capability presented:

outside-grant-receipt.json · completejson
{
  "action": {
    "parameter_hash": "0fa7ff0ae661a7951272ef82d54a46a50c4fd53d74aaf2a690fb251f6a0cd17c",
    "parameters": {
      "order": "order-77"
    }
  },
  "boundary_class": "prevent",
  "capability_id": "cap-01a12eff-a57c-76f4-941e-55509841be30",
  "content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
  "decision": {
    "guard": "kernel",
    "reason": "requested tool refund on server pricing is not in capability scope",
    "verdict": "deny"
  },
  "id": "0c8dc3e4b6ebe95b7cca1d994c77dbd62e293e9cb1f091a6ac3a5a82eb00b020",
  "kernel_key": "950290ccefab6f9bda88b641d01da9f40ed2e79f35826cd3603d8573092a17a1",
  "metadata": {
    "attribution": {
      "delegation_depth": 0,
      "issuer_key": "950290ccefab6f9bda88b641d01da9f40ed2e79f35826cd3603d8573092a17a1",
      "subject_key": "52a5c22df743e029cce231fecbb4511ff17585bbba83b5692fb387b7150a9635"
    },
    "chio_receipt_signing_nonce": "rcpt-01a12eff-a59a-76e5-aeb6-bd501519d29c",
    "receipt_context": {
      "request_id": "681a3f44-f263-4eeb-9257-1ef612a2bbbe"
    }
  },
  "policy_hash": "1b173021c071b4370dcb81bbb77ac30499a85e5824ddbfecc860b286f1fee7db",
  "receipt_kind": "mediated_decision",
  "redaction_mode": "none",
  "signature": "711743b586fdd3c47dc715be667721588d0e4037d7c9c15e645e1795da95c1eac1e7bcb6b16163dd57e55808f7a30bdddf2d0842b9dafde50f0ec2f1b1b96109",
  "timestamp": 1791789868,
  "tool_name": "refund",
  "tool_origin": "caller_executed",
  "tool_server": "pricing",
  "trust_level": "mediated"
}

Continue with Audit the pricing tools to filter the two denials in this store, or Verify Receipts Offline for the checks in detail.

Add resources and prompts

A native service can also expose named resources and reusable prompts with NativeResource and NativePrompt from the adapter's native module. The service answers them through read_resource and get_prompt, which do not pass through the governed tool-call path above, so keep anything sensitive in a tool.

Advertise a price in minor units

src/service.rs · on the quote toolrust
.per_invocation_price(1, "USD")

For USD, 1 minor unit is USD 0.01 per quote, the price Basalt pays in the Quickstart. The declaration advertises a price in the manifest; it collects nothing and sets no spending limit by itself. Budgets and metering adds spending authority and accounting.

Next: extend the same service