# A2A-Swap
> Lightweight constant-product AMM designed for autonomous AI agents on Solana.
> Zero human involvement required by default.
**Program ID:** `8XJfG4mHqRZjByAd7HxHdEALfB8jVtJVQsdhGEmysTFq`
**Network:** Solana mainnet-beta
**Protocol fee:** 0.020% (to on-chain treasury PDA)
**LP fee range:** 1–100 bps (0.01%–1.00%, set per pool)
---
## Why A2A-Swap instead of Jupiter?
| **Autonomy** | Fully headless — no browser, no widget | Designed for human UIs |
| **Agent-native API** | Typed Rust + TypeScript SDKs with `async/await` | REST aggregator, complex routing |
| **Approval mode** | Built-in co-signature (`approve_and_execute`) for human-in-the-loop | Not available |
| **LP auto-compound** | Fees compound to LP shares on-chain, no harvest tx | Not available |
| **Fee model** | Transparent: 0.020% protocol + pool LP fee | Variable aggregator fees |
| **Capability card** | Machine-readable JSON constant embedded on-chain | Not available |
| **Dependencies** | Single program, no oracle required | Dozens of routing programs |
| **Gas** | ~40k CU per swap | 200k–600k CU via routed hops |
A2A-Swap is designed for the case where the **caller is a bot**: no UI, deterministic paths, stable fees, and SDKs that emit typed structs.
---
## Installation
### CLI (Rust)
```bash
# From crates.io
cargo install a2a-swap-cli
# Or build from source
git clone https://github.com/liqdlad-rgb/a2a-swap
cd a2a-swap
cargo build --release -p a2a-swap-cli
# Binary at ./target/release/a2a-swap
```
Pre-built binaries for Linux, macOS, and Windows are also available on the
[Releases page](https://github.com/liqdlad-rgb/a2a-swap/releases).
### TypeScript SDK
```bash
npm install @liqdlad/a2a-swap-sdk @solana/web3.js @solana/spl-token
# or
yarn add @liqdlad/a2a-swap-sdk @solana/web3.js @solana/spl-token
```
### Rust SDK
```toml
[dependencies]
a2a-swap-sdk = "0.1"
```
---
## Quick start
```bash
# Set your keypair and RPC
export A2A_KEYPAIR=~/.config/solana/id.json
export A2A_RPC_URL=https://api.mainnet-beta.solana.com
# Preview a swap without spending funds
a2a-swap simulate --in SOL --out USDC --amount 1000000000
# Execute the swap
a2a-swap convert --in SOL --out USDC --amount 1000000000
# Check your LP positions and accrued fees
a2a-swap my-fees
```
---
## Command reference
All commands accept `--rpc-url <URL>` and `--keypair <PATH>` flags (or env vars
`A2A_RPC_URL` / `A2A_KEYPAIR`). Add `--json` to any command for machine-readable output.
### `simulate` — Preview a swap
```
a2a-swap simulate --in <TOKEN> --out <TOKEN> --amount <ATOMIC_UNITS>
```
Prints a full fee breakdown without sending a transaction. No keypair needed.
```bash
a2a-swap simulate --in SOL --out USDC --amount 1000000000
```
```
─── Simulate: SOL → USDC ─────────────────────────────────────────────
Pool HqXr…v7
Direction A → B
Amount in 1,000,000,000 SOL
Protocol fee 20,000 (0.020%)
LP fee 2,994 (0.30% of net)
After fees 999,977,006
Estimated out 149,988,450 USDC
Effective rate 0.149988
Price impact 0.013%
Reserve in 9,999,000,000
Reserve out 1,500,000,000
```
**Token symbols:** `SOL`, `USDC`, `USDT` are resolved automatically.
Any other token accepts a raw base-58 mint address.
---
### `convert` — Execute a swap
```
a2a-swap convert --in <TOKEN> --out <TOKEN> --amount <ATOMIC_UNITS> [--max-slippage <PCT>]
```
Simulates, applies slippage tolerance, then builds and sends the transaction.
Direction is auto-detected — swap in either direction without any extra flag.
```bash
# Swap 1 SOL for USDC (0.5% slippage tolerance is the default)
a2a-swap convert --in SOL --out USDC --amount 1000000000
# Tighter slippage
a2a-swap convert --in SOL --out USDC --amount 1000000000 --max-slippage 0.1
# Reverse direction
a2a-swap convert --in USDC --out SOL --amount 150000000
# Require webhook approval before sending (human-in-the-loop)
a2a-swap convert --in SOL --out USDC --amount 1000000000 \
--approval-mode webhook --webhook-url https://mybot.example.com/approve
# Machine-readable output (for agent pipelines)
a2a-swap convert --in SOL --out USDC --amount 1000000000 --json
```
Output includes the full fee breakdown and transaction signature:
```
─── Swap Executed ────────────────────────────────────────────────────
─── Fee Breakdown ────────────────────────────────
Sold 1,000,000,000 SOL
Protocol fee 20,000 (0.020%)
LP fee 2,994 (0.30% of net)
─── Output ───────────────────────────────────────
Received (est.) 149,988,450 USDC
Min accepted 149,238,558 (0.5% slippage)
─── Transaction ──────────────────────────────────
Signature 5hGp…xQ
Explorer https://explorer.solana.com/tx/5hGp…xQ
```
---
### `create-pool` — Create a new pool
```
a2a-swap create-pool --pair <A-B> --initial-price <FLOAT> [--fee-bps <1-100>]
```
Creates a constant-product pool. The PDA controls the vaults — no human key holds authority.
```bash
# Create a SOL/USDC pool with 0.30% LP fee, initial spot hint of 185 USDC/SOL
a2a-swap create-pool --pair SOL-USDC --initial-price 185 --fee-bps 30
# Print a ready-to-run `provide` command to seed with 1 SOL of liquidity
a2a-swap create-pool --pair SOL-USDC --initial-price 185 --seed-amount 1000000000
# Custom mints
a2a-swap create-pool --pair <mintA>-<mintB> --initial-price 1.0 --fee-bps 10
```
```
─── Pool Created ─────────────────────────────────────────────────────
Pool HqXr…v7
Authority 3vZp…kM (PDA — no human key)
Vault A 8BnT…rQ
Vault B 2cLf…wP
Fee rate 30 bps (0.30%)
Signature 5hGp…xQ
─── Next step ───────────────────────────────────
a2a-swap provide --pair SOL-USDC --amount 1000000000 --amount-b 185000000
```
> `--initial-price` is a convenience hint that generates the seed command.
> It is not stored on-chain; the actual price is set by the first deposit.
---
### `provide` — Add liquidity
```
a2a-swap provide --pair <A-B> --amount <ATOMIC_UNITS> [--amount-b <ATOMIC_UNITS>]
[--auto-compound] [--compound-threshold <ATOMIC_UNITS>]
```
Deposits token pairs proportionally and returns LP shares recorded in a `Position` account.
- **First deposit** — provide both `--amount` and `--amount-b` to set the initial price.
- **Subsequent deposits** — omit `--amount-b`; the program computes it from live reserves.
- **`--auto-compound`** — reinvests accrued fees as additional LP shares instead of accumulating them for manual claim.
```bash
# Seed empty pool: 1 SOL + 185 USDC (sets initial price)
a2a-swap provide --pair SOL-USDC --amount 1000000000 --amount-b 185000000
# Add to existing pool (amount-b computed from live reserves)
a2a-swap provide --pair SOL-USDC --amount 500000000
# Enable auto-compounding (compound when fees exceed 0.001 SOL)
a2a-swap provide --pair SOL-USDC --amount 500000000 \
--auto-compound --compound-threshold 1000000
```
---
### `pool-info` — Inspect a pool
```
a2a-swap pool-info --pair <A-B>
```
Read-only — no keypair required, no transaction sent.
```bash
a2a-swap pool-info --pair SOL-USDC
```
```
─── Pool: SOL / USDC ─────────────────────────────────────────────────
Pool HqXr…v7
Authority 3vZp…kM (PDA)
Reserve A 9,999,000,000 SOL
Reserve B 1,500,000,000 USDC
LP supply 3,872,983
Fee rate 30 bps (0.30%)
Spot price 0.150015 USDC per SOL
```
---
### `my-positions` — List LP positions
```
a2a-swap my-positions
```
Lists all `Position` accounts owned by the agent keypair — LP shares, pool, and auto-compound settings.
Run `my-fees` to see claimable fee balances.
---
### `my-fees` — Check claimable fees
```
a2a-swap my-fees
```
Lists all LP positions and their accrued fees. No transaction sent — safe to poll frequently.
```
─── Positions & Fees ─────────────────────────────────────────────────
[0] HqXr…v7 pool: SOL/USDC
LP shares 1,936,491
Fees A 12,450 SOL
Fees B 1,870 USDC
Auto-compound enabled (threshold: 1,000,000)
─────────────────────────────────────────────────────
Total fees A 12,450
Total fees B 1,870
```
### Remove liquidity and claim fees (SDK)
`remove_liquidity` and `claim_fees` are available as on-chain instructions and are
fully supported by both SDKs. CLI wrappers (`remove` and `claim-fees`) are planned
for v1.1.
**TypeScript:**
```typescript
await client.removeLiquidity(keypair, { mintA, mintB, lpShares: 500_000_000n, minA: 0n, minB: 0n });
await client.claimFees(keypair, { mintA, mintB });
```
**Rust:**
```rust
client.remove_liquidity(&payer, RemoveParams { mint_a, mint_b, lp_shares: 500_000_000, min_a: 0, min_b: 0 }).await?;
client.claim_fees(&payer, ClaimParams { mint_a, mint_b }).await?;
```
---
## Zero-human execution
A2A-Swap is designed to be called entirely by autonomous agents without any human approval:
1. **No browser / widget** — every operation is a single RPC call or CLI command.
2. **PDA authority** — pool vaults are controlled by a derived program address, not a human keypair. No admin can rug.
3. **Deterministic fees** — protocol fee (0.020%) and LP fee (pool-specific, 1–100 bps) are fixed on-chain. No aggregator routing surprises.
4. **Atomic execution** — a swap, liquidity deposit, or fee claim is a single transaction. No multi-step approval flow unless you opt in.
5. **Machine-readable capability card** — agents can introspect the protocol's capabilities without any off-chain registry:
```rust
use a2a_swap::A2A_CAPABILITY_CARD;
let card: serde_json::Value = serde_json::from_str(A2A_CAPABILITY_CARD).unwrap();
// card["capabilities"]["autonomousExecution"] == true
// card["feeModel"]["protocolFeeBps"] == 20
```
---
## How bots earn fees as LPs
Bots can earn passive income by acting as liquidity providers:
```
1. create-pool (one time per token pair)
2. provide --auto-compound
3. …swaps happen, fees accumulate in pool vaults…
4. fees auto-compound into LP shares (or claim manually via SDK)
```
### Fee accounting
Fees are tracked with a Q64.64 accumulator (`fee_growth_global`) stored on the `Pool` account.
Each `Position` stores a `fee_growth_checkpoint` at the time of the last deposit or claim.
```
claimable_fees_A = lp_shares × (fee_growth_global_A − checkpoint_A) >> 64
claimable_fees_B = lp_shares × (fee_growth_global_B − checkpoint_B) >> 64
```
Fees **stay in the vault** (they increase k), so no tokens are moved until you claim. This means:
- LPs benefit from slightly improved swap rates over time (growing reserves).
- `claim_fees` (via SDK) transfers tokens out of the vault to your wallet.
- `--auto-compound` converts fees_owed to additional LP shares — no vault transfer needed.
### Auto-compound flow
```
claim_fees (auto_compound=true, threshold met)
└── fees_owed_a / fees_owed_b > compound_threshold
└── new_lp_shares = min(
fees_owed_a × total_lp / reserve_a,
fees_owed_b × total_lp / reserve_b
)
└── position.lp_shares += new_lp_shares
└── pool.lp_supply += new_lp_shares
└── fees_owed reset to 0
(no tokens leave the vault)
```
---
## Protocol fee model
Every swap deducts two fees from `amount_in`:
```
protocol_fee = amount_in × 20 / 100_000 (0.020%, goes to treasury PDA)
net = amount_in − protocol_fee
lp_fee = net × fee_rate_bps / 10_000 (0.01%–1.00%, stays in vault)
after_fees = net − lp_fee
amount_out = reserve_out × after_fees / (reserve_in + after_fees)
```
| Fee | Rate | Destination |
|-----|------|-------------|
| Protocol fee | 0.020% fixed | Treasury PDA token account |
| LP fee | 1–100 bps (pool-specific) | Pool vaults (accrues to LPs) |
The protocol fee is skimmed before LP fee calculation to keep the LP math clean.
LPs only earn on the net amount after the protocol fee.
---
## Approval mode (human-in-the-loop)
For agents that require a human or co-agent co-signature before executing swaps:
```bash
# Require webhook approval before sending
a2a-swap convert --in SOL --out USDC --amount 1000000000 \
--approval-mode webhook --webhook-url https://mybot.example.com/approve
```
Or call `approve_and_execute` directly — both the agent keypair **and** a designated
approver must sign the **same transaction**. No on-chain pending state is created.
```typescript
// TypeScript — build and co-sign
const ix = approveAndExecuteIx({ pool, agent: agentKey, approver: approverKey,
amountIn: 1_000_000_000n, minAmountOut: 148_000_000n, aToB: true });
const tx = new Transaction().add(ix);
await sendAndConfirmTransaction(conn, tx, [agentKeypair, approverKeypair]);
```
---
## Integration examples
### ElizaOS (TypeScript)
```bash
elizaos plugins add plugin-a2a-swap
# or manually: npm install @liqdlad/plugin-a2a-swap
```
```typescript
import { a2aSwapPlugin } from '@liqdlad/plugin-a2a-swap';
import { AgentRuntime } from '@elizaos/core';
const runtime = new AgentRuntime({
plugins: [a2aSwapPlugin],
// ...
});
```
Registers five actions automatically:
| `A2A_SIMULATE_SWAP` | "simulate swap", "estimate swap", "quote swap" |
| `A2A_SWAP` | "swap tokens", "trade tokens", "exchange tokens" |
| `A2A_PROVIDE_LIQUIDITY` | "provide liquidity", "add liquidity", "add to pool" |
| `A2A_POOL_INFO` | "pool info", "pool stats", "check pool" |
| `A2A_MY_FEES` | "my fees", "check fees", "claimable fees" |
### TypeScript SDK
```typescript
import { A2ASwapClient } from '@liqdlad/a2a-swap-sdk';
import { Keypair, PublicKey } from '@solana/web3.js';
const client = A2ASwapClient.mainnet();
// Simulate (no wallet needed)
const sim = await client.simulate({
mintIn: new PublicKey('So11111111111111111111111111111111111111112'),
mintOut: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
amountIn: 1_000_000_000n,
});
console.log(`Estimated out: ${sim.estimatedOut}, impact: ${sim.priceImpactPct.toFixed(3)}%`);
// Execute swap
const result = await client.convert(keypair, {
mintIn: new PublicKey('So11111111111111111111111111111111111111112'),
mintOut: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
amountIn: 1_000_000_000n,
maxSlippageBps: 50,
});
console.log(`Signature: ${result.signature}`);
// Pool info
const info = await client.poolInfo(mintA, mintB);
console.log(`Spot price: ${info.spotPrice.toFixed(6)}, reserves: ${info.reserveA} / ${info.reserveB}`);
// Check fees
const fees = await client.myFees(keypair.publicKey);
console.log(`Claimable: ${fees.totalFeesA} tokenA, ${fees.totalFeesB} tokenB`);
```
### Rust SDK
```rust
use a2a_swap_sdk::{A2ASwapClient, SimulateParams, SwapParams};
use solana_sdk::{pubkey::Pubkey, signature::{read_keypair_file, Signer}};
use std::str::FromStr;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = A2ASwapClient::mainnet();
let payer = read_keypair_file("~/.config/solana/id.json")?;
// Simulate (read-only, no funds needed)
let sim = client.simulate(SimulateParams {
mint_in: Pubkey::from_str("So11111111111111111111111111111111111111112")?,
mint_out: Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")?,
amount_in: 1_000_000_000,
}).await?;
println!("Estimated out: {}, impact: {:.3}%", sim.estimated_out, sim.price_impact_pct);
// Execute swap
let result = client.convert(&payer, SwapParams {
mint_in: Pubkey::from_str("So11111111111111111111111111111111111111112")?,
mint_out: Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")?,
amount_in: 1_000_000_000,
max_slippage_bps: 50,
}).await?;
println!("Signature: {}", result.signature);
Ok(())
}
```
---
## Error reference
| `PoolNotFound` | No pool for this mint pair | Run `create-pool` first |
| `NoLiquidity` | Pool exists but reserves are 0 | Run `provide` to seed it |
| `AmountBRequired` | First deposit needs explicit `--amount-b` | Pass `--amount-b` to set the initial price |
| `SlippageExceeded` | Output below minimum | Increase `--max-slippage` or reduce `--amount` |
| `MathOverflow` | Amount too large for u64 math | Reduce `--amount` |
| `Unauthorized` | Approver signature missing | Ensure both agent and approver keys are present |
---
## Roadmap
### v0.1 (current — mainnet)
- [x] Constant-product AMM (x·y=k), deployed on mainnet-beta
- [x] LP fee auto-compound
- [x] Approval mode (co-signature, no on-chain state)
- [x] CLI — `simulate`, `convert`, `create-pool`, `provide`, `my-positions`, `pool-info`, `my-fees`
- [x] TypeScript SDK (`@liqdlad/a2a-swap-sdk`) published to npm
- [x] ElizaOS plugin (`plugin-a2a-swap`) published to elizaos registry
- [x] Rust SDK (`a2a-swap-sdk`) published to crates.io
- [x] CLI (`a2a-swap-cli`) published to crates.io
- [x] Integration test suite (29/29 passing)
- [x] SOL/USDC pool live on mainnet
### v1.0 (planned)
- [ ] **CLI `remove` and `claim-fees` commands** — wrappers for the existing on-chain instructions
- [ ] **Time-weighted average price (TWAP)** oracle — 30-slot ring buffer, readable by any agent
- [ ] **Permissioned pools** — optional LP whitelist (enterprise / DAO use)
- [ ] **Multi-hop routing** — chain two pools in one transaction for pairs without a direct pool
- [ ] **Webhook approval backend** — reference server for `--approval-mode webhook`
- [ ] **Security audit**
---
## License
MIT — see [LICENSE](./LICENSE)