Unofficial Rust client for the Avo Portfolio (Market) API on Solana.
⚠️ This is a community-maintained, unofficial SDK. It is not affiliated with, maintained by, or endorsed by Avo (avo.so). It is a Rust port of the official TypeScript SDK,@avodotso/market-sdk.
Built for market-makers: bootstrap an agent, run rebalances, push NAV updates — over plain HTTP, through one client. It is not an on-chain SDK; every call goes to the Avo REST API, which signs the on-chain instructions server-side.
The single entry point is AvoMarketClient, which implements the MarketClient
trait covering the full market-maker journey: create → operate → close.
# Cargo.toml
[dependencies]
avo-rust-sdk = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }A Tokio runtime is required — the client is built on reqwest (hyper + Tokio) and
uses Tokio primitives internally. Other runtimes (async-std / smol) need a compatibility
shim such as async-compat.
Or:
cargo add avo-rust-sdk tokioThe library re-exports the one solana type it exposes as avo_rust_sdk::Pubkey, so
self-custodial callers need no solana crate of their own. Only the create-market flow
needs solana-sdk (for Keypair) and solana-client (to broadcast) — see the
examples.
You hold a bearer token (from finalize_create_market / register_market); the API
holds the agent's signing key encrypted at rest. You never touch a private key.
use avo_rust_sdk::client::{AvoMarketAgent, AvoMarketClient, MarketClient, MarketClientConfig};
use avo_rust_sdk::types::{NavRequest, RebalanceRequest, WeightInput};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = AvoMarketClient::new(
MarketClientConfig::new()
.with_avo_api("https://portfolio-contract-api-service-dev.up.railway.app")
.with_agent(AvoMarketAgent::Bearer {
token: std::env::var("AGENT_BEARER")?,
pubkey: None,
}),
);
// Live valuation of every vault in the market (50 bps pricing slippage).
let value = client.get_value(Some(50)).await?;
println!("{value:#?}");
// Rebalance to a new target weight vector (bps sum to 10_000).
client
.rebalance(
RebalanceRequest::builder()
.weights(vec![
WeightInput::builder().asset("base").weight_bps(0).build(),
WeightInput::builder()
.asset("So11111111111111111111111111111111111111112")
.weight_bps(10_000)
.build(),
])
.slippage_bps(100)
.deadline_secs(180)
.build(),
)
.await?;
// Push fresh NAV on chain (omit total_value_base to let the API quote live).
client.update_nav(NavRequest::builder().build()).await?;
Ok(())
}You hold the agent's Ed25519 secret key; the SDK mints short-lived signed tokens on every call window.
use avo_rust_sdk::auth::LocalSigner;
use avo_rust_sdk::client::{AvoMarketAgent, AvoMarketClient, MarketClientConfig};
use avo_rust_sdk::Pubkey; // re-exported — no solana crate needed
let signer = LocalSigner::new(pubkey /* Pubkey */, seed /* [u8; 32] */);
let client = AvoMarketClient::new(
MarketClientConfig::new()
.with_avo_api("https://…")
.with_agent(AvoMarketAgent::SelfCustodial { pubkey, signer: Box::new(signer) }),
);External signers (KMS / HSM / hardware wallet) plug in by implementing the
Signer trait instead of LocalSigner.
More use cases → examples/
Run any with cargo run --example <name>:
| Example | What it shows |
|---|---|
healthz, list_markets, market_snapshot, agent_identity |
Zero-auth public reads |
mint_token, external_signer, error_handling |
Auth & error mechanics (offline) |
operate_bearer, operate_sig |
Operate a market in each auth mode + rebalance |
create_market, create_market_manual |
Create a market (permissionless flow, one-shot & step-by-step) |
register_market |
Register a market you built on-chain yourself |
All methods are on the MarketClient trait (AvoMarketClient is the concrete impl),
except create_market_with_shadow_portfolio, which is an inherent method on
AvoMarketClient.
| Method | Endpoint |
|---|---|
prepare_create_market(req) |
POST /v1/markets/create/prepare |
finalize_create_market(req) |
POST /v1/markets/create/finalize |
register_market(req) |
POST /v1/markets/register |
create_market_with_shadow_portfolio(req, &signer, submit) |
one-shot prepare → sign → finalize |
| Method | Endpoint |
|---|---|
healthz() |
GET /healthz |
list_markets(req) |
GET /v1/markets |
get_market_events(pda, req) |
GET /v1/markets/{pda}/events |
get_nav_history(pda, req) |
GET /v1/markets/{pda}/nav-history |
get_identity(pubkey) |
GET /v1/agents/identity/{pubkey} |
attach_identity(req) |
POST /v1/agents/identity/attach |
| Method | Endpoint |
|---|---|
get_market() |
GET /v1/market |
get_assets() |
GET /v1/assets |
get_value(slippage_bps) |
GET /v1/value |
get_quote(req) |
GET /v1/quote |
update_nav(req) |
POST /v1/nav |
rebalance(req) |
POST /v1/rebalance |
simulate_rebalance(req) |
POST /v1/rebalance/simulate |
add_asset(req) |
POST /v1/assets |
remove_asset(mint, req) |
DELETE /v1/assets/{mint} |
get_own_identity() |
GET /v1/agents/identity/{self} |
Requests use a builder (SomeRequest::builder().field(..).build()); optional fields
are omitted from the wire body when unset. Responses expose new(..) + Default.
Atomic-unit amounts are rust_decimal::Decimal (quoted decimal strings on the wire).
Authenticated methods work in two mutually-exclusive modes, selected by the
AvoMarketAgent you pass to MarketClientConfig::with_agent. Both resolve to the same
agent identity server-side and grant identical access.
pub enum AvoMarketAgent {
// Custodial: the API holds the key; you hold a one-time bearer.
Bearer { token: String, pubkey: Option<Pubkey> },
// Self-custodial: you hold the key; the SDK mints signed tokens per call.
SelfCustodial { pubkey: Pubkey, signer: Box<dyn Signer + Send + Sync> },
}- Bearer stamps a static
Authorization: Bearer <token>. - Sig mints
Authorization: Sig <pubkey>.<expiresAtMs>.<sigBase58>over a canonical message, caching the token until ~30s before expiry. TTL defaults to 600s; override withMarketClientConfig::with_token_ttl_secs.
External signers. Implement the async Signer trait for KMS / HSM / hardware
wallets — the SDK hands you the message bytes and you return a 64-byte Ed25519 signature.
LocalSigner is the in-process implementation.
Helpers (mint tokens / build attach messages outside the client):
avo_rust_sdk::auth::{mint_token, build_attach_message, canonical_auth_message}.
Gotcha: the agent pubkey must be registered with the API (by creating or registering a market) before Sig/Bearer auth works — otherwise the API returns
BAD_AUTH / "Agent pubkey not registered". Seedocs/api-discovery.md.
Every fallible method returns Result<T, AvoError> (aliased AvoResult<T>):
pub enum AvoError {
// A structured API/SDK error (HTTP status + machine code).
Sdk { status: u16, code: String, message: String, details: Option<serde_json::Value> },
// A transport failure (connection refused, DNS, non-JSON body).
Transport { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
}Branch with the accessors err.status() -> Option<u16>, err.code() -> Option<&str>,
err.is_transport() -> bool:
match client.rebalance(req).await {
Ok(res) => { /* … */ }
Err(e) if e.is_transport() => eprintln!("network: {e}"),
Err(e) => eprintln!("api error [{:?}] {:?}: {e}", e.status(), e.code()),
}Codes the SDK raises itself (everything else comes from the API's error envelope, e.g.
BAD_AUTH, RATE_LIMITED):
| Code | When |
|---|---|
SERVICE_UNCONFIGURED |
No API base URL was configured. |
AGENT_NOT_CONFIGURED |
A protected method was called on an unauthenticated client (or get_own_identity without a known pubkey). |
INVALID_FUNDING_SIGNATURE |
The funding-tx signer returned an empty signature. |
HTTP_<status> |
A non-2xx response with no parseable error envelope. |
Semantic versioning. The crate version tracks the upstream @avodotso/market-sdk
version it ports — 0.1.5 here mirrors upstream 0.1.5. A scheduled CI workflow
(.github/workflows/upstream-watch.yml) checks
npm daily and opens a sync issue when upstream publishes a new version.
Licensed under the ISC License, matching the upstream SDK.
Maintained by @pr0m3th3usEx.
Not affiliated with Avo. For the official product, see avo.so.