aspens 0.8.0

Aspens crosschain trading SDK
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# Aspens SDK

[![crates.io](https://img.shields.io/crates/v/aspens.svg)](https://crates.io/crates/aspens)
[![docs.rs](https://docs.rs/aspens/badge.svg)](https://docs.rs/aspens)

A comprehensive SDK and CLI tools for interacting with an [Aspens Markets Stack](https://docs.aspens.xyz).

> Looking for the TypeScript SDK? It is published on npm as [`@aspensprotocol/sdk-ts`]https://www.npmjs.com/package/@aspensprotocol/sdk-ts.

The core library is published on crates.io as [`aspens`](https://crates.io/crates/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

Install the latest `aspens-cli` and `aspens-repl` (Linux x86_64 / aarch64, macOS Apple Silicon):

```sh
curl -fsSL https://raw.githubusercontent.com/aspensprotocol/sdk/main/install.sh | sh
```

Overrides: `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](https://github.com/aspensprotocol/sdk/releases)
alongside a `SHA256SUMS` file.

Or build from source (also the path for `aspens-admin`):

```sh
cargo install --locked --git https://github.com/aspensprotocol/sdk aspens-cli aspens-repl
```

## Available Commands

### Trader commands (`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.

### Admin commands (`aspens-admin`)

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 |

## Project Structure

This is a Cargo workspace with four main components:

- **`aspens/`** - Core Rust library crate with trading logic and gRPC client
- **`aspens-cli/`** - Command-line interface binary for scripted operations
- **`aspens-repl/`** - Interactive REPL binary for manual trading
- **`aspens-admin/`** - Administrative CLI for stack configuration (chains, tokens, markets)

## Prerequisites

1. **Install Rust:**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

2. **Install Just (Optional but Recommended):**

[Just](https://github.com/casey/just) is a command runner that simplifies common development tasks.

```bash
brew install just          # macOS
cargo install just         # Any platform
```

3. **Configure environment:**
```bash
cp .env.sample .env        # Copy the template
# Edit .env with your configuration (ASPENS_MARKET_STACK_URL, TRADER_PRIVKEY, etc.)
```

## Building

```bash
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 only
```

## Usage

### 1. As a Rust Library

Install from crates.io:
```bash
cargo add aspens
```

Or add it manually to your `Cargo.toml`:
```toml
[dependencies]
aspens = "0.8"
```

Full client (gRPC + trading commands + RPC submission):
```rust
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):
```bash
cargo add aspens --no-default-features --features evm,solana
```
```toml
[dependencies]
aspens = { version = "0.8", default-features = false, features = ["evm", "solana"] }
```
```rust
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-agnostic `derive_order_id`, destination-token parsing.
- **`aspens::evm`** — MidribV3 `sol!` 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.

### 2. Interactive Mode (REPL)

```bash
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> quit
```

### 3. Scripted Mode (CLI)

```bash
cargo 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 250
```

### Post-only orders

Pass `--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.

```bash
# 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-only
```

In Rust:

```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).

### 4. Admin CLI

```bash
# 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 -- status
```

## Cargo Feature Flags

The `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.8"`
- **Lean EVM signing**: `aspens = { version = "0.8", default-features = false, features = ["evm"] }`
- **Lean Solana signing**: `aspens = { version = "0.8", default-features = false, features = ["solana"] }`
- **Both chains, no client runtime**: `aspens = { version = "0.8", default-features = false, features = ["evm", "solana"] }`

The `aspens-cli`, `aspens-repl`, and `aspens-admin` binaries all depend
on the default feature set.

## Just Commands

```bash
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
```

## Architecture

### Core Library (`aspens/`)

- **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) and `Wallet::Solana` (Ed25519) behind one signing interface
- **Chain dispatch** - `ChainClient` routes RPC calls to Alloy (EVM) or `solana-client` based 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 the `client` feature 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

### CLI Binary (`aspens-cli/`)

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`:

```bash
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.

### REPL Binary (`aspens-repl/`)

Interactive Read-Eval-Print Loop for manual trading with command history and session state.

### Admin Binary (`aspens-admin/`)

Administrative CLI for managing stack configuration with EIP-712 signature authentication.

## Decimal Handling

Aspens handles tokens with different decimal places across chains. The SDK works in "pair decimals" format internally. See `decimals.md` for detailed conversion examples.

## Token Assumptions

**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 exactly `amount`. 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 `tradeBalance` during `_depositAndLock`, cause the Aspens vault to under-collect fees, and under-deliver `SETTLE_AND_WITHDRAW` payouts.
- **No rebasing.** Tokens whose balances change between two reads of `balanceOf` (AMPL-style, aTokens in rebase mode) break the `balanceBefore`/`balanceAfter` reconciliation 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 `lockedTradeBalance` until 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`:

1. Read the token's `transfer` implementation — confirm `_balances[from] -= amount` is the only debit.
2. Run a probe transfer (any amount) and check `balanceAfter == balanceBefore - amount` on both sides.
3. Confirm `balanceOf` is 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.

### Solana-specific notes

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.



## Documentation

- [Decimal Conversion Guide]decimals.md - Understanding decimal handling
- [CHANGELOG.md]CHANGELOG.md - Release notes per version
- [AGENTS.md]AGENTS.md - Architecture guide for development

## Versioning

The `aspens` crate follows [Semantic Versioning](https://semver.org/). 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 in
  `aspens::{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 in [`CHANGELOG.md`]CHANGELOG.md.
- **Internal modules** — `aspens::grpc` (and any module marked
  `#[doc(hidden)]` or `pub(crate)`) are implementation details and may
  change in any release. Generated proto bindings — `aspens::attestation::*`
  and the `*_pb` modules beside each command group
  (`commands::config::config_pb`, `commands::trading::arborter_pb`,
  `commands::auth::auth_pb`) — track the upstream `protos/` repo and follow
  its compatibility, not the SDK's. Each generated file is compiled exactly
  once; a second `include!` 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.

## License

This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.