A comprehensive SDK and CLI tools for interacting with an Aspens Markets Stack.
The core library is published on crates.io as aspens.
The aspens-cli, aspens-repl, and aspens-admin binaries live in this
workspace; aspens-cli and aspens-repl ship as prebuilt release binaries
(see below), and all three can be built from source.
Install the latest aspens-cli and aspens-repl (Linux x86_64 / aarch64, macOS Apple Silicon):
curl -fsSL https://raw.githubusercontent.com/aspensprotocol/sdk/main/install.sh | shOverrides: INSTALL_DIR=<dir> picks the location (default /usr/local/bin on
Linux, ~/.local/bin on macOS); ASPENS_VERSION=vX.Y.Z pins a specific release.
Binaries are attached to each GitHub release
alongside a SHA256SUMS file.
Or build from source (also the path for aspens-admin):
cargo install --locked --git https://github.com/aspensprotocol/sdk aspens-cli aspens-repl| Command | Description |
|---|---|
config [--output-file <path>] |
Fetch and display the configuration from the server (saves to .json / .toml if --output-file is set) |
deposit <network> <token> <amount> |
Deposit tokens to make them available for trading |
withdraw <network> <token> <amount> |
Withdraw tokens to a local wallet |
buy-market <market> <amount> --quote-budget <quote> |
Send a market BUY order (executes at best available price). --quote-budget is required: a market buy gives quote and has no price to size that with, so the budget — not <amount> — is what bounds the spend and what gets collateralised. |
buy-limit <market> <amount> <price> [--post-only] |
Send a limit BUY order (executes at specified price or better). With --post-only, the order is rejected if it would cross at submission — guarantees maker-side execution. |
sell-market <market> <amount> |
Send a market SELL order (executes at best available price). No budget needed: a sell gives base, so <amount> IS its budget. |
sell-limit <market> <amount> <price> [--post-only] |
Send a limit SELL order (executes at specified price or better). See --post-only above. |
buy-marketable <market> <amount> [--slippage-bps <bps>] |
CLI only. Snapshot the resting book, cap slippage above best ask (default 50 bps = 0.5%), submit as a buy-limit. Turns "take the top of book with a slippage cap" into the equivalent priced order — useful when you want an explicit price ceiling rather than a quote budget. |
sell-marketable <market> <amount> [--slippage-bps <bps>] |
CLI only. Same as buy-marketable, but capping slippage below best bid. |
cancel-order <market> <side> <order_id> |
Cancel an existing order by its ID |
stream-orderbook <market> [--historical] [--trader <addr>] |
Stream orderbook entries in real-time |
stream-trades <market> [--historical] [--trader <addr>] |
Stream executed trades in real-time |
balance |
Fetch the current balances for all supported tokens across all chains |
status |
Show current configuration and connection status |
trader-public-key |
Get the public key and address for the trader wallet |
signer-public-key [--chain-network <network>] |
Get the signer public key(s) for the trading instance (filtered to a chain network if provided) |
get-attestation [--nonce <hex>] [--save-quote <file>] [-o text|json] |
Fetch the signer's TD Quote; the caller-chosen nonce is bound into the quote's REPORTDATA (challenge-response); --save-quote writes the raw quote for offline verify-attestation --quote |
All commands above are available in both aspens-cli and aspens-repl, except buy-marketable / sell-marketable which are CLI-only. The REPL also adds a quit command to exit the session.
Most commands below require a JWT (set via --jwt, ASPENS_JWT in .env, or the aspens-admin login flow).
| Command | Description |
|---|---|
init-admin --address <eth-address> |
Initialize the first admin on a fresh stack (no JWT required) |
login [--chain-id <id>] |
Authenticate via EIP-712 signature using ADMIN_PRIVKEY and obtain a JWT |
update-admin <eth-address> |
Update the admin address |
set-chain --architecture … --canonical-name … --network … --chain-id … --rpc-url … --factory-address … [--explorer-url …] [--instance-signer-address …] |
Add or update a chain entry |
delete-chain <network> |
Remove a chain from the configuration |
rpc list <network> |
List a chain's current RPC endpoint set (masked; unauthenticated) |
rpc set <network> --endpoint <endpoint> [--endpoint <endpoint> …] |
Replace a chain's complete RPC endpoint set (full replace, priority = flag order) |
rpc probe <network> <url> [--scheme none|header|basic|bearer] [--key <key>] [--secret <secret>] |
Probe a candidate RPC endpoint before committing it with rpc set; never stored |
set-token --network … --name … --symbol … --address … --decimals … |
Add or update a token on a chain |
delete-token --network <network> --symbol <symbol> |
Remove a token from a chain |
set-market --base-network … --quote-network … --base-symbol … --quote-symbol … --base-address … --quote-address … --pair-decimals … |
Add or update a market (register both tokens with set-token first — the market takes its token decimals from them) |
delete-market <market_id> |
Remove a market |
deploy-contract <network> [--fees <bps>] |
Deploy a trade contract on a chain (fee in basis points, default 0) |
set-trade-contract --address <addr> --chain-network <network> |
Register an existing trade contract address on a chain |
set-operator-fee --chain-network <network> --recipient <addr> --bps <bps> |
Set an instance's operator fee (recipient + bps). Arborter-submitted, so it fails on-chain unless the instance's operator_admin is the arborter signer — which current contracts refuse |
rotate-operator-admin --chain-network <network> --new-admin <addr> |
Rotate an instance's operator_admin key. Same limitation as set-operator-fee |
delete-trade-contract <chain_network> |
Remove the trade contract association from a chain |
version |
Show server version information |
status |
Show current configuration and connection status |
admin-public-key |
Get the public key and address for the admin wallet (from ADMIN_PRIVKEY) |
balances |
Show balances for owner, signers, and contracts across all chains |
This is a Cargo workspace with four main components:
aspens/- Core Rust library crate with trading logic and gRPC clientaspens-cli/- Command-line interface binary for scripted operationsaspens-repl/- Interactive REPL binary for manual tradingaspens-admin/- Administrative CLI for stack configuration (chains, tokens, markets)
- Install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh- Install Just (Optional but Recommended):
Just is a command runner that simplifies common development tasks.
brew install just # macOS
cargo install just # Any platform- Configure environment:
cp .env.sample .env # Copy the template
# Edit .env with your configuration (ASPENS_MARKET_STACK_URL, TRADER_PRIVKEY, etc.)just build # Build entire workspace
just release # Build release version
just build-lib # Build core library only
just build-cli # Build CLI only
just build-repl # Build REPL only
just build-admin # Build Admin CLI onlyInstall from crates.io:
cargo add aspensOr add it manually to your Cargo.toml:
[dependencies]
aspens = "0.7"Full client (gRPC + trading commands + RPC submission):
use aspens::{AspensClient, DirectExecutor};
#[tokio::main]
async fn main() -> eyre::Result<()> {
let client = AspensClient::builder()
.with_url("http://localhost:50051")?
.build()?;
// Use trading operations...
Ok(())
}Stateless signing only (no gRPC, no tokio, no RPC client — e.g. browser
via wasm-bindgen, edge workers, or a service that submits orders over
its own transport):
cargo add aspens --no-default-features --features evm,solana[dependencies]
aspens = { version = "0.7", default-features = false, features = ["evm", "solana"] }use aspens::orders::derive_order_id;
use aspens::evm::envelope_signing_digest;
use aspens::solana::withdrawal_voucher_signing_message;
// The canonical order id, derived from the intent fields. The server derives
// the same id from the signed order, so it is never sent alongside it.
let order_id = derive_order_id(
&user_pubkey_bytes, nonce, origin_chain, dest_chain,
&input_token_bytes, &output_token_bytes, input_amount, output_amount,
);
// EVM: the EIP-191 digest recovered against for the outer order envelope.
let digest = envelope_signing_digest(&encoded_send_order_request);
// Solana: the exact bytes the arborter signed for a withdrawal voucher, which
// go in the paired Ed25519 verify instruction.
let msg = withdrawal_voucher_signing_message(
&instance, &account, &mint, amount, nonce, deadline,
)?;Under the optimistic shadow ledger, orders never lock on-chain, so there is no per-order EIP-712 lock signature: a client signs the outer envelope over the encoded request, and that is all.
The pure modules:
aspens::orders— chain-agnosticderive_order_id, destination-token parsing.aspens::evm— MidribV3sol!bindings, the EIP-191 envelope digest, native-token sentinel helpers.aspens::solana— PDA derivations, instruction builders, borsh payload encoder, Ed25519 precompile ix, well-known program ids.
cargo run --bin aspens-repl
# Inside the REPL
aspens> help
aspens> balance
aspens> deposit base-sepolia USDC 1000
aspens> buy-market USDC/USDT 100 --quote-budget 250
aspens> quitcargo run --bin aspens-cli -- balance
cargo run --bin aspens-cli -- deposit base-sepolia USDC 1000
cargo run --bin aspens-cli -- buy-market USDC/USDT 100 --quote-budget 250Pass --post-only to buy-limit / sell-limit to guarantee your order
adds liquidity rather than taking it. If the price would cross the
opposing side of the book at submission, arborter returns
FAILED_PRECONDITION and the order is never booked, so no collateral is
committed and you can resubmit at a different price.
# Post a maker-only bid at 100. If the best ask is ≤ 100, the order
# is rejected and you can retry at 99 (or below).
aspens-cli buy-limit USDC/USDT 1.5 100 --post-only
# Same on the sell side: rejected if there's a resting bid at ≥ 200.
aspens-cli sell-limit USDC/USDT 1.5 200 --post-onlyIn Rust:
use aspens::commands::trading::send_order;
let response = send_order::send_order_with_wallet(
stack_url,
market_id,
1, // 1 = BUY
"1.5".into(), // quantity
Some("100".into()), // limit price (required for post-only)
&wallet,
config,
true, // post_only
false, // hidden
None, // quote_budget: only a market BUY states one
).await?;Post-only is incompatible with market orders (the SDK pre-rejects
post_only=true with price=None before signing) and with the
buy-marketable / sell-marketable CLI variants (which are designed
to cross — the CLI hard-codes post_only=false for them).
# Initialize admin (first time only)
cargo run --bin aspens-admin -- init-admin --address 0xYourAddress
# Login to get JWT
cargo run --bin aspens-admin -- login
# Admin commands (JWT set in .env or via --jwt flag)
cargo run --bin aspens-admin -- set-chain --network base-sepolia ...
cargo run --bin aspens-admin -- set-token --network base-sepolia --symbol USDC ...
cargo run --bin aspens-admin -- statusThe aspens crate exposes eight feature flags. evm, solana, client,
and trader are on by default; admin, fce, dcap, and dcap-fetch
are opt-in. Consumers can trim down to just what they need:
| Feature | What it pulls in | When to keep / drop |
|---|---|---|
evm |
aspens::evm (sol! bindings, EIP-712 hasher, envelope signer) + aspens::orders. Tiny — alloy-primitives/alloy-sol-types/alloy-signer-local. |
Keep if you build or sign EVM orders. |
solana |
aspens::solana (PDA derivations, instruction builders, borsh payload encoder, Ed25519 precompile ix). Pulls solana-sdk, borsh, bs58. |
Keep if you build or sign Solana orders. |
client |
Full runtime: AspensClient, trading commands, gRPC (tonic/prost), async runtime (tokio), RPC submission (solana-client, alloy). |
Keep for the CLI/REPL/admin experience or anything that talks to the Aspens stack. Drop it for browser / embedded / offline-signing. |
trader |
commands::trading (deposit/withdraw/order/balance/streams) + table formatting (comfy-table). |
Keep for the trading command surface (needs client). |
admin |
commands::trading, commands::admin, commands::auth (chain/token/market registration, JWT login). No extra deps. |
Keep for the admin command surface (needs client). |
fce |
The FCE direct-action transport (aspens::fce): POST /direct + poll. Pulls reqwest, tokio. |
Keep only when talking to a stack behind the FCE proxy — parked deployment mode. |
dcap |
The relying-party DCAP quote verifier (tdx_verify::dcap), pure-Rust dcap-qvl. |
Keep if you verify TDX attestations locally. |
dcap-fetch |
dcap + collateral fetching over the PCS/PCCS REST API (tdx_verify::collateral): reqwest, asn1_der, pem, urlencoding. |
Keep for the verify-attestation CLI command. |
Common configurations:
- Default (everything):
aspens = "0.7" - Lean EVM signing:
aspens = { version = "0.7", default-features = false, features = ["evm"] } - Lean Solana signing:
aspens = { version = "0.7", default-features = false, features = ["solana"] } - Both chains, no client runtime:
aspens = { version = "0.7", default-features = false, features = ["evm", "solana"] }
The aspens-cli, aspens-repl, and aspens-admin binaries all depend
on the default feature set.
just # List all available commands
just build # Build the project
just test # Run all tests
just test-lib # Run library tests only
just fmt # Format code
just check # Check code style
just lint # Run linter
just clean # Clean build artifacts- AspensClient - Main client with builder pattern for configuration
- Trading operations - Deposit, withdraw, buy, sell, balance queries across EVM and Solana chains
- Curve-agnostic wallet -
Wallet::Evm(secp256k1) andWallet::Solana(Ed25519) behind one signing interface - Chain dispatch -
ChainClientroutes RPC calls to Alloy (EVM) orsolana-clientbased on chain architecture - Executor pattern - Async/sync execution strategies
- gRPC client - Protocol buffer communication with an Aspens Market Stack
- Client-side order helpers (
aspens::orders/aspens::evm/aspens::solana) — stateless builders for the gRPC order payload:derive_order_id, the EIP-191 envelope digest (EVM), PDA derivations, borsh voucher payloads, Ed25519 precompile ix (Solana). Available without theclientfeature for browser / embedded callers. - EVM integration - MidribV3 ABI bindings (shared JSON artifacts with arborter), Alloy signer
- Solana integration - Midrib Anchor program: Anchor discriminators, PDA seeds, SPL token flow
Command-line interface for scripted trading operations.
It also carries the two operator commands that must NOT go through the
arborter, set-withdraw-epoch-cap and set-settle-epoch-cap:
OPERATOR_ADMIN_PRIVKEY_SOLANA=<base58 or id.json contents> \
aspens-cli set-withdraw-epoch-cap <network> <token> <cap>
OPERATOR_ADMIN_PRIVKEY_SOLANA=<base58 or id.json contents> \
aspens-cli set-settle-epoch-cap <network> <token> <cap>This arms the Solana midrib program's per-(instance, mint) per-epoch
withdrawal ceiling. cap is in human units (scaled by the token's decimals),
and 0 means unlimited — the shipped default for every mint, matching
MidribV3.setWithdrawEpochCap on EVM. The epoch is 9,000 slots (~1 hour) and
the window is tumbling, not sliding, so up to 2 × cap can leave across a
boundary; for at most X per hour, set cap = X/2.
set-settle-epoch-cap arms the matching ceiling on settlement: the most
settle_batch may credit to accounts (the sum of its positive deltas) per
(instance, mint) per epoch, across all batches — MidribV3.setSettleEpochCap
on EVM. Same units, epoch and 0 = unlimited. Size it above the venue's
legitimate hourly settled volume: a batch that would exceed it is refused on
chain, and the arborter holds that token's settlement until it fits (the
epoch rolls over or the cap is raised).
The authority for both is the instance's on-chain operator_admin, so the
commands build, sign and submit the transaction locally with an offline key
(OPERATOR_ADMIN_PRIVKEY_SOLANA, base58 or JSON keypair) — deliberately not
the trader or admin key, and never the arborter's. The program refuses an
operator_admin equal to the TEE signer, so the TEE cannot raise the caps that
bound it; an instance created before that check may still carry one, and the
commands warn when they see it. --help on each command restates all of this.
Interactive Read-Eval-Print Loop for manual trading with command history and session state.
Administrative CLI for managing stack configuration with EIP-712 signature authentication.
Aspens handles tokens with different decimal places across chains. The SDK works in "pair decimals" format internally. See decimals.md for detailed conversion examples.
Important: Aspens only supports tokens with standard ERC-20 / SPL semantics. Adding a non-compliant token to a market — via aspens-admin set-token or the admin-console — will produce incorrect balances, fee leakage, or stuck funds. The contracts do not detect non-compliant tokens; gating happens here, in market configuration.
A token is safe to add only if all of the following hold:
- Standard transfer semantics. A
transfer(to, amount)reduces the sender's balance by exactlyamount. No transfer hooks that re-enter or opportunistically revert. - No fee-on-transfer. Tokens that charge a fee on transfer (reflection tokens, deflationary tokens) silently shift cost onto the user's existing
tradeBalanceduring_depositAndLock, cause the Aspens vault to under-collect fees, and under-deliverSETTLE_AND_WITHDRAWpayouts. - No rebasing. Tokens whose balances change between two reads of
balanceOf(AMPL-style, aTokens in rebase mode) break thebalanceBefore/balanceAfterreconciliation used throughout the contract. Use the wrapped, non-rebasing variant (e.g. wstETH, not stETH). - No supply-pause that strands open orders. Pausable tokens are tolerable as long as pauses are short-lived; pauses of a duration longer than the cancel-and-unlock window can strand
lockedTradeBalanceuntil the pause lifts. - Blocklist tokens (USDC-style) are accepted with caveats. Funds remain correctly accounted for, but a blocklisted address cannot withdraw or settle out until removed from the list.
Quick checklist before running aspens-admin set-token:
- Read the token's
transferimplementation — confirm_balances[from] -= amountis the only debit. - Run a probe transfer (any amount) and check
balanceAfter == balanceBefore - amounton both sides. - Confirm
balanceOfis a pure function of stored state, not a function of total supply.
If any of these checks fails, do not add the token. Common safe examples: USDC, USDT (on chains where USDT does not enable fee-on-transfer), WBTC, WETH, DAI, most stablecoins.
The on-chain midrib program uses the legacy SPL Token program, not Token-2022. Mints owned by the Token-2022 program (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) will fail deserialization at every entry point — by design, since Token-2022's transfer-fee, interest-bearing, and confidential-transfer extensions would all break the program's deposited += amount accounting.
Withdrawal vouchers carry an args.deadline slot. The on-chain tombstone PDA makes a voucher single-use regardless of deadline, but keeping the deadline tight (current_slot + 600, roughly 4 minutes at 400ms slots) limits the window between the arborter signing and the holder submitting.
- Decimal Conversion Guide - Understanding decimal handling
- CHANGELOG.md - Release notes per version
- AGENTS.md - Architecture guide for development
The aspens crate follows Semantic Versioning. The
workspace is pre-1.0, so the conventions in effect today are:
- Patch releases (
0.4.x→0.4.y) — bug fixes, performance work, internal refactors. No source-breaking changes to public items inaspens::{client, wallet, orders, evm, solana, decimals}or to the re-exports at the crate root. - Minor releases (
0.4.x→0.5.0) — may include breaking changes to the public API surface (renames, signature changes, removals). Notable changes are recorded inCHANGELOG.md. - Internal modules —
aspens::grpc(and any module marked#[doc(hidden)]orpub(crate)) are implementation details and may change in any release. Generated proto bindings —aspens::attestation::*and the*_pbmodules beside each command group (commands::config::config_pb,commands::trading::arborter_pb,commands::auth::auth_pb) — track the upstreamprotos/repo and follow its compatibility, not the SDK's. Each generated file is compiled exactly once; a secondinclude!of one would mint a parallel, incompatible set of types rather than an alias. - CLI / REPL / Admin binaries — version-bumped together with the
library. Flag and command renames are called out in
CHANGELOG.md.
When in doubt about whether a change is breaking, check the changelog entry for the target version.
This project is licensed under the Apache License 2.0. See the LICENSE file for details.