Skip to main content
Verified against aomi-sdk@main on 2026-06-04.
This page is about authoring an Aomi App: how the crate is laid out, what goes in each file, and how you wire it together. An App is a Rust crate that compiles to a dynamic plugin the Aomi runtime hot loads. It wraps an external API as a small set of typed tools, ships a preamble that tells the model how to use them, and declares which host capabilities it needs. When you are ready to compile and ship, follow the forward links at the bottom of this page. This page does not cover the build or deploy steps.
Throughout this page, App (capitalized) means the deployed unit: a cdylib crate the runtime loads. The earlier docs talked about aomi_chat::CoreAppBuilder. That is an internal runtime crate and is not how you author a plugin. The real authoring model is the one below: the dyn_aomi_app! macro plus the DynAomiTool trait from the public aomi-sdk crate.

What you are building

The Aomi SDK lets you wrap any crypto API as a dynamic plugin. The runtime loads your compiled plugin and routes chat to it. The apps in the SDK repo show the range you can cover:

DeFi protocols

Wrap a DEX, lending market, or staking protocol as tools the chat can drive. See defillama, morpho.

Prediction markets

Market discovery, search, and trading flows. See polymarket, kalshi.

Cross-chain intents

Bridge and intent-order clients. See khalani, across, lifi.

Social and accounts

Feeds, posts, user data, wallet and account tooling. See x, neynar, para.

The standard file split

Every App follows the same layout across three files. Keep this split even for small apps. It keeps the surface the model sees easy to scan and keeps API details out of your tool logic.
my-app/
├─ Cargo.toml
├─ aomi.toml          (contributor apps only — see "The aomi.toml manifest")
└─ src/
   ├─ lib.rs          App registration + preamble (via dyn_aomi_app!)
   ├─ client.rs       HTTP client + typed models + tool arg structs
   └─ tool.rs         Tool implementations (DynAomiTool)
FileWhat it owns
src/lib.rsThe preamble string and the dyn_aomi_app! registration. This is your manifest surface: app name, version, the tool list, and the host namespaces you depend on.
src/client.rsHTTP client setup, auth headers, response models, and the typed argument structs your tools accept. Outside API details stay here.
src/tool.rsThe tool implementations. Each tool implements DynAomiTool, carries a description the model reads, and maps client responses into stable JSON.
The fastest way to start is to copy sdk/examples/app-template-http from the SDK repo and adapt it. It is the canonical reference for this layout.

src/lib.rs: registration and preamble

lib.rs is short on purpose. It declares the other modules, holds the preamble, and calls dyn_aomi_app! to register the App. Here is the full lib.rs from the HTTP template:
use aomi_sdk::*;

mod client;
mod tool;

const PREAMBLE: &str = r#"## Role
You are a lightweight example app for market lookup and simple HTTP API integration.

## Purpose
- Demonstrate the recommended Aomi app file structure
- Show how to wrap a public JSON API with typed tools
- Keep the example free of private infrastructure assumptions

## Workflow
1. Use `search_coins` to find a CoinGecko asset ID.
2. Use `get_coin_price` to fetch the current USD price for that asset.
"#;

dyn_aomi_app!(
    app = client::HttpJsonExampleApp,
    name = "http-json-example",
    version = "0.1.0",
    preamble = PREAMBLE,
    tools = [client::SearchCoins, client::GetCoinPrice,],
    namespaces = []
);
The dyn_aomi_app! macro is the single registration point. It generates the plugin entry the runtime looks for, so you never write #[no_mangle] by hand. Its fields:
FieldMeaning
appYour app type. It is a small struct (often unit-like) that holds any shared state.
nameThe app slug. Kebab-case. This is how the runtime and the URL refer to your App.
versionThe app version string.
preambleThe system prompt that shapes behavior (see below).
toolsThe list of tool types the model can call. Keep it small: 3 to 8 tools is typical.
namespacesThe host capability sets your App depends on. [] for a read only HTTP wrapper (see “Declaring host namespaces”).

Writing the preamble

The preamble is the system prompt. It is the single most important thing you author, because it decides whether the model picks the right tool at the right time. Write it as plain prose with clear headings, not as code. A strong preamble does four things:
1

States the role

One or two sentences on what the App is and what it must never do. The DeFiLlama app opens with “You are a read only analyst… never to execute trades.”
2

Maps capabilities to tools

List what the App can do and name the exact tool for each job, so the model learns the mapping. “Token prices: defillama_get_token_price for current price.”
3

Pins down identifiers and conventions

Spell out the formats the API expects: coin id schemes, protocol slugs, chain names, timestamp units. This is where most tool call errors are prevented.
4

Gives workflow guidance

Tell the model how to chain tools for common questions. “Comparison questions: start with list_protocols, then dig into the winners with get_protocol_tvl.”
Here is the shape, trimmed from the real defillama app:
const PREAMBLE: &str = r#"## Role
You are **DeFi Data Assistant**, a read-only analyst backed by DeFiLlama's free public API.
Your job is to answer questions about token prices, protocol size, chain activity, and yield
opportunities — never to execute trades.

## Capabilities
- **Token prices** -- `defillama_get_token_price` for current price.
- **Protocols** -- `defillama_list_protocols` ranks protocols by current TVL.
- **Chains** -- `defillama_get_chain_tvl` returns the chain leaderboard by TVL.

## Conventions
- **No auth.** DefiLlama is a free public API; no key is needed.
- **Coin identifiers** use `coingecko:<id>` (e.g. `coingecko:bitcoin`) or `<chain>:<address>`.
- **Protocol slugs** match DefiLlama URLs (e.g. `aave-v3`, `uniswap`, `lido`).

## Formatting
- Format USD amounts: TVL > $1B as `$X.XXB`, > $1M as `$XXX.XM`, otherwise `$X,XXX`.
"#;

Defining tools

You are the MyCoinDex trading assistant.

## Capabilities
- Check real-time token prices
- View user portfolio and P&L
- Execute trades (with user confirmation)
- List available trading pairs

## Rules
- Always confirm with the user before executing a trade
- Show prices in USD unless the user requests otherwise
- If you don't have data for a token, say so clearly
- Never fabricate prices or portfolio data

## Tone
- Professional but approachable
- Concise responses unless the user asks for detail
- Use bullet points for lists of data
Tools live in src/tool.rs. Each tool is a type that implements DynAomiTool. The trait ties together your app type, the typed argument struct, the name and description the model reads, and a run function that does the work and returns JSON.
use crate::client::*;
use aomi_sdk::*;
use serde_json::{Value, json};

pub(crate) struct SearchCoins;

impl DynAomiTool for SearchCoins {
    type App = HttpJsonExampleApp;
    type Args = SearchCoinsArgs;
    const NAME: &'static str = "search_coins";
    const DESCRIPTION: &'static str =
        "Search CoinGecko for matching assets and return coin ids developers can use in follow-up price calls.";

    fn run(
        _app: &HttpJsonExampleApp,
        args: Self::Args,
        _ctx: DynToolCallCtx,
    ) -> Result<Value, String> {
        let client = CoinGeckoClient::new()?;
        let value = client.get_json("/search", &[("query", args.query.as_str())])?;
        // ...normalize into stable JSON...
        Ok(json!({ "query": args.query, "matches": value }))
    }
}
The pieces that matter:
  • NAME is the function name the model calls. Prefer names shaped by intent like search_*, get_*, build_*, and submit_* over raw endpoint wraps.
  • DESCRIPTION is read by the model when it decides whether to call the tool. Write it for the model, not for a human reader.
  • run returns Result<Value, String>. On error, return a short actionable string. Normalize upstream API errors so the model gets a clean message, not a raw stack.

Typed, documented arguments

The argument struct lives in src/client.rs. Derive Deserialize and JsonSchema, and put a doc comment on every field. Those doc comments become the parameter descriptions the model sees, so they directly shape how it fills the call.
use aomi_sdk::schemars::JsonSchema;
use serde::Deserialize;

#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct SearchCoinsArgs {
    /// Free-form search query such as `bitcoin`, `eth`, or `dogecoin`
    pub(crate) query: String,
}

#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct GetCoinPriceArgs {
    /// CoinGecko coin id such as `bitcoin` or `ethereum`
    pub(crate) coin_id: String,
}
Doc comments on argument fields are read by the model. A vague field comment is a vague tool. Give a concrete example value in each one, the way the template does.

The client and models

src/client.rs owns the HTTP plumbing so your tools stay readable. Build the client once, set a timeout, normalize errors into strings, and keep the response shapes here.
pub(crate) struct CoinGeckoClient {
    pub(crate) http: reqwest::blocking::Client,
}

impl CoinGeckoClient {
    pub(crate) fn get_json(&self, path: &str, query: &[(&str, &str)]) -> Result<Value, String> {
        let url = format!("{API_BASE}{path}");
        let response = self.http.get(&url).query(query).send()
            .map_err(|e| format!("CoinGecko request failed: {e}"))?;
        let status = response.status();
        let body = response.text().unwrap_or_default();
        if !status.is_success() {
            return Err(format!("CoinGecko error {status}: {body}"));
        }
        serde_json::from_str(&body).map_err(|e| format!("CoinGecko decode failed: {e}"))
    }
}
For full trait signatures (DynAomiTool, DynToolCallCtx, the dyn_aomi_app! macro, and the aomi_sdk::testing helpers like TestCtxBuilder and run_tool), see the SDK Reference.

Declaring host namespaces

The namespaces field in dyn_aomi_app! declares which host capabilities your App needs. A read only HTTP wrapper needs none, so it uses namespaces = [], like the template above. An App that reads chain state or stages transactions declares a namespace, the way the defillama app does:
dyn_aomi_app!(
    app = tool::DefiLlamaApp,
    name = "defillama",
    version = "0.1.0",
    preamble = PREAMBLE,
    tools = [tool::GetTokenPrice, tool::ListProtocols, tool::GetChainTvl],
    namespaces = ["evm-core"]
);
Host capabilities are a public contract, not private infrastructure. Apps that execute transactions may assume the host runtime exposes tools such as:
Host toolWhat it does
view_stateEncode calldata from ABI arguments and run a read only eth_call.
run_txEncode calldata and simulate a call that changes state without staging it.
stage_txStage an EVM transaction for later simulation and signing.
simulate_batchSimulate one or more staged transactions before prompting a wallet.
commit_txAsk the user wallet to sign and broadcast one staged transaction.
evm_commit_messageAsk the user wallet to sign an EIP-712 typed-data message.
The transaction model is always stage first, then simulate, then commit. Describe the host capabilities you rely on in your tool descriptions and preamble. Do not refer to private namespaces, and do not assume a hidden fallback network. If a host does not implement a capability, it surfaces that absence rather than silently redirecting.
Apps that execute across several steps (like polymarket or khalani) return a ToolReturn with route hints instead of a bare JSON value, so the host can chain a wallet signature back into the next tool call. The full route hint contract lives in the SDK repo’s docs/host-interop.md and the SDK Reference.

The aomi.toml manifest

If you are building a community or partner App in your own source repo, you also author an aomi.toml. This manifest tells the deploy tooling where your App ships and how the backend should load it. (Official Apps that live in the SDK repo do not use aomi.toml; they ship through the repo’s release pipeline instead.)
[app]
name         = "my-cool-app"            # slug — kebab-case, becomes the release tag
display_name = "My Cool App"            # human-readable name
platform     = "community"              # the platform you publish to
git          = "https://github.com/aomi-labs/community-apps"
public       = true                     # visible to all backend users

# Optional. Which backend tier may load this release.
# Omit to default to ["staging"]. Set to ["prod"] once tested,
# or ["staging", "prod"] for both.
# server_tags = ["staging"]
Field reference:
FieldRequiredMeaning
nameyesThe App slug. Kebab-case. Becomes the release tag.
display_nameyesA name shown to people, easy to read.
platformyesThe publishing platform, for example community.
gityesThe platform repo your App ships into.
publicyesWhether the App is visible to all backend users.
server_tagsnoThe backend tiers this build may load on. Defaults to ["staging"].
access_tokennoOnly for private platform repos. A $ENV_VAR reference, never a literal.
access_token must point at an environment variable, not a literal token. Write access_token = "$MY_GH_TOKEN". A literal like "ghp_xxxx" is rejected at parse time so a committed config can never leak a secret. The public community-apps repo does not need this field at all; omit it.

Pin the SDK version

Your Cargo.toml must pin aomi-sdk to the exact version the platform expects, and your crate must build as a cdylib:
[package]
name = "my-cool-app"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
aomi-sdk   = "=0.1.20"          # match platform.json's required_sdk_version
serde      = { version = "1", features = ["derive"] }
serde_json = "1"
=0.1.20 is the pin the community-apps platform expects today. Always confirm against platform.json’s required_sdk_version in the platform repo before you deploy; a mismatch fails CI with an sdk_version mismatch error.

Authoring guidelines

  • Prefer one App crate per external product or ecosystem.
  • Keep the toolset small. 3 to 8 tools per App is typical for a clean workflow.
  • Keep tool arguments typed and documented with JsonSchema. The doc comments are read by the model.
  • Normalize upstream API errors into short actionable strings.
  • Keep assumptions about one host out of your prompts.
  • If your App needs signing or execution, depend only on the public host capabilities above.

Next steps

You have authored the crate. Now compile it, test it, and ship it.

Compile and run

Use the aomi-build CLI to compile your plugin and aomi-run to exercise it locally before you ship.

Deploy and activate

Use aomi-git deploy to stage your source into the platform repo, then request activation so the backend loads it.

SDK Reference

Full trait definitions: DynAomiTool, the dyn_aomi_app! macro, the host-interop contract, and the testing helpers.

How it works

See the full request flow once your App is loaded on the runtime.
Last modified on June 4, 2026