AchSwap SDK (Local Signer)
@achswap/mcp-sdk is the piece that actually holds your keys and signs
transactions. It runs entirely on your machine and broadcasts signed transactions
to ARC. By default it runs in local mode (builds transactions on your device, no
AchSwap server needed); optionally it can use AchSwap's hosted builder in remote
mode. This is what makes AchSwap self-custodial: your private key never
leaves your computer.
For the hosted server it connects to, see MCP Server.
Why install the SDK?
- Your keys never leave your machine. The server only ever sees unsigned transactions.
- You approve every trade. Writes queue for your approval by default; the AI cannot broadcast on its own.
- Local transaction building. Switch to
localmode and nothing touches AchSwap's servers at all. - Vault-sealed wallet. The keystore password is a 256-bit random value sealed by your OS credential store, so copying the Achswap folder does not reveal your wallet. A 12-word phrase and a portable recovery bundle back it up.
Install
npm install -g @achswap/mcp-sdk
Or run without a global install (opencode/Claude configs use this):
npx -y @achswap/mcp-sdk serve
Verify:
achswap --version
Quick start
# 1. Create the wallet — offers a passphrase, then prints your 12-word phrase
achswap init
# 2. (only if you skipped it above) add the passphrase later
achswap passphrase set
# 3. Save a portable backup somewhere offline (USB, encrypted folder)
achswap export-recovery /path/to/usb/achswap-recovery.json
# 4. Fund the printed address with native USDC on ARC Testnet
# 5. Connect your AI client (writes the MCP config for you, in local mode)
achswap install opencode # or: claude | codex | cursor
# 6. Check how you are protected
achswap security
Writes queue for approval by default. Release them with achswap approve <id>,
or let the agent trade on its own with achswap set automation trade — transfers
and approvals will still wait for you.
The wallet address is always available to the agent via get_wallet_address
(no arguments needed — the SDK fills in your address).
Modes
| Mode | ACHSWAP_MODE | Tx building | Backend |
|---|---|---|---|
| Local (recommended) | local | On your machine | none (works out of the box) |
| Remote (optional) | remote | On AchSwap's hosted server | mcp-api.achswap.app (must be deployed) |
Local mode is the recommended default and the most private: the SDK builds and
signs entirely on-device with no AchSwap server involved, so it works immediately.
Remote mode is optional and just means "use AchSwap's hosted builder"; signing is
still local. Remote mode requires the hosted Worker to be deployed — it is not
live yet, so use local mode for now.
Configuration
All config is env > config.json (in ~/.achswap) > built-in defaults.
| Variable | Default | Meaning |
|---|---|---|
ACHSWAP_MODE | local | remote or local (local works with no backend) |
ACHSWAP_AUTOMATION | manual | manual / trade / full — may only lower the configured level |
ACHSWAP_AUTO_CREATE_WALLET | false | Allow a missing wallet to be created silently on first use |
ACHSWAP_ALLOW_HEADLESS_KEY_ACCESS | false | CI only — read key material with no terminal |
ACHSWAP_PASSPHRASE | — | Your vault passphrase (only if you set one) |
ACHSWAP_MCP_SERVER_URL | https://mcp-api.achswap.app | Hosted backend URL |
ACHSWAP_KEYSTORE_DIR / ACHSWAP_HOME | ~/.achswap | Where the keystore lives |
ACHSWAP_PASSWORDstill works as an alias forACHSWAP_PASSPHRASE. Never put either into an AI client config file — those files are plaintext on disk.achswap installwrites onlyACHSWAP_MODEand the remote URL.
Environment variables cannot grant permissions
Permission settings live in ~/.achswap/config.json and are changed with
achswap set or the Settings menu. An environment variable can turn a permission
off, but never on.
This matters because achswap install writes into your AI client's config file,
and that file is plain JSON the agent itself can edit. If the environment won, a
prompt-injected agent could raise its own automation level by appending one line to its
own MCP config and waiting for a restart — without ever touching the vault.
Any refused attempt is reported by achswap security:
▲ ACHSWAP_AUTOMATION=full was ignored.
Something in this process's environment tried to raise automation above
what config.json allows.
The rule covers automation, autoCreateWallet and allowHeadlessKeyAccess.
ACHSWAP_AUTOMATION may lower the level but never raise it. It does not apply to
ACHSWAP_PASSPHRASE (a secret, not a permission) or to mode / rpcUrl /
chainId.
Wallet, encryption & recovery
How your key is protected
The keystore password is not something you choose and not stored in the Achswap folder. It is a 256-bit random value that only exists in memory while a transaction is being signed.
~/.achswap/keystore.json V3 keystore ← scrypt(N=2^18) + AES-128-CTR
encrypted with a 256-bit random password
~/.achswap/vault.json AES-256-GCM envelope holding that password
key = HKDF-SHA512( device secret ‖ scrypt(your passphrase, N=2^18) )
device secret 32 random bytes in your OS credential store — never on disk
passphrase optional, in your head — never on disk
The device secret lives in Windows Credential Manager, macOS Keychain, or Secret Service (libsecret) depending on your platform. On Windows the vault is additionally wrapped with DPAPI, binding it to your Windows logon session.
This is the point: copying the whole ~/.achswap folder is not enough to steal
the wallet. There is no low-entropy password in it to guess offline — an attacker
also needs your OS user session, and your passphrase if you set one.
Unlock modes
| Mode | What it takes to sign | How to get it |
|---|---|---|
device | Your OS credential store | Default after achswap init |
device + passphrase | OS credential store and your passphrase | achswap passphrase set (recommended) |
passphrase | Your passphrase only | Automatic on systems with no credential store |
Run achswap security to see which mode you are in and get warned about
anything weak.
Device mode protects your files, not your running session. The credential store hands the device secret to any process running as your OS user — that is how the SDK itself unlocks.
To blunt the common version of that attack, a device-only vault will not unlock
in a non-interactive process. An agent shelling out to node, an injected
prompt, or a remote session gets an error telling it to ask you. Revealing the
recovery phrase is stricter still: it always needs an interactive terminal, with
no scriptable flag.
This raises the bar but is not a wall — a determined attacker can allocate a
terminal. A passphrase is the one factor your machine does not hold, which is why
achswap init offers it up front and achswap security warns until you set one.
Set ACHSWAP_ALLOW_HEADLESS_UNLOCK=true only for automation you trust.
If no credential store is available and you give no passphrase, the SDK refuses to create a wallet rather than writing a generated password to disk.
Three ways to recover
Security must not lock you out. Any one of these restores your wallet:
1. Portable recovery bundle (best for disaster recovery)
achswap export-recovery /path/to/usb/achswap-recovery.json
# on any other machine:
achswap import-recovery /path/to/usb/achswap-recovery.json
Sealed with a passphrase you choose, and deliberately not tied to any device, so it works on a brand-new computer. Keep it offline — anyone with the file and its passphrase controls the wallet.
2. 12-word recovery phrase
achswap recover -m "word1 word2 … word12"
Shown once at achswap init. Store it offline. Never paste it into chat, logs, or
a website.
3. The vault on your own machine — as long as your OS credential store and (if set) your passphrase are intact.
achswap backupis not disaster recovery. It copieskeystore.json+vault.json, but that copy only opens on the same machine and OS account, because the device secret stays in the credential store and is never written to disk. For a backup that survives losing the machine, useexport-recoveryor the 12-word phrase.
Upgrading from an older SDK
Versions up to 1.0.7 stored an auto-generated keystore password in plaintext at
~/.achswap/.session-pw, right next to keystore.json — so anyone who copied the
folder got both halves.
achswap migrate
This runs automatically when the SDK starts, and also on demand. It decrypts your
key with the old password, re-encrypts it under a fresh 256-bit password, seals
that in the vault, and overwrites and deletes .session-pw. Your address and
private key do not change.
The old password must be treated as compromised, which is why the keystore is re-encrypted rather than simply re-sealed. After migrating, export a fresh recovery bundle.
Signing: manual by default
One setting decides how much the agent may do on its own.
automation | The agent can | You approve |
|---|---|---|
manual (default) | build and quote | everything |
trade | swap, wrap, add/remove liquidity | transfers, approvals |
full | everything | nothing |
achswap set automation trade
Transfers and approvals are held back at trade. transfer_token,
transfer_native, approve_token and approve_for_router queue for you even
though everything else signs itself. These hand value, or the right to take it,
to another address — and an unlimited approval is simply a transfer the spender
can execute later, which is why both sit at the same level. Only full includes
them.
The agent cannot work around this by queueing a transfer and then calling
confirm_transaction; that path is gated identically.
Setting the level is what grants the unlock
At trade or full you are saying the signer may open the vault with no one
present, so it does — for signing only. There is no second switch to find.
Reading key material is a different act and keeps its own rule: show-phrase,
export-recovery and a script calling loadWallet() always require a
terminal, at every automation level. Signing is bounded (right chain, right
destination, amount matching the request, transfer gate applied); extracting a
key is not.
If your vault has a passphrase, unattended signing additionally needs
ACHSWAP_PASSPHRASE in the signer's environment — otherwise writes queue and
achswap security tells you exactly that, rather than silently doing nothing.
Approving queued writes
- In a terminal:
achswap approve <id>— it prints destination, amount, chain, calldata selector, expected effects and the arguments the tool was called with, then asks you to confirm. - In chat:
confirm_transaction, available attradeandfullonly, and never for transfers or approvals unless the level isfull.
CLI command reference
achswap is the command-line control panel for the SDK. It manages the wallet,
configuration, and the local MCP server your AI client connects to. All commands:
| Command | Purpose |
|---|---|
achswap init [-p PASS] | Create the wallet (prints a 12-word recovery phrase once) |
achswap security | Show how the wallet is protected; flag anything weak |
achswap (no args) | Interactive menu — settings, pending, wallet, security |
achswap show-phrase | Re-display the 12-word recovery phrase |
achswap pending --clear | Discard the queue (after an integrity warning) |
achswap delete-wallet | Permanently delete keystore, vault, and device secret |
achswap passphrase set|remove|status | Manage the passphrase (second unlock factor) |
achswap export-recovery <path> | Write a portable, passphrase-sealed backup |
achswap import-recovery <path> | Restore from that backup on any machine |
achswap migrate | Upgrade a pre-1.1 wallet off the plaintext .session-pw |
achswap recover -m "w1 … w12" [-p PASS] | Restore the wallet from its recovery phrase |
achswap backup [dir] | Copy keystore + vault locally (same machine only) |
achswap address | Print your wallet address (no unlock needed) |
achswap balance | Print your native USDC balance |
achswap status | Show settings + any running MCP server |
achswap running (ps) | List running Achswap MCP processes |
achswap config | Print the resolved config as JSON |
achswap set <key> <value> | Change a persisted setting |
achswap install <client> | Inject the MCP config into an AI client |
achswap pending | List queued (manual-mode) transactions |
achswap approve <id> | Sign + broadcast a pending transaction |
achswap serve [opts] | Start the local MCP server (normally launched by your client) |
achswap run --tool <t> --args <json> | Call one tool and exit (no AI client needed) |
achswap | Interactive mode |
achswap --version / --help | Print version / list commands |
Config set via
achswap setis written to~/.achswap/config.json. Environment variables (ACHSWAP_*) always override it, so they win in case of conflict.
Wallet lifecycle
achswap init — creates ~/.achswap/keystore.json plus ~/.achswap/vault.json
and prints the 12-word recovery phrase. Refuses to overwrite an existing
keystore. In a terminal it offers a passphrase first, explaining the trade-off.
achswap init→ offers a passphrase; press Enter to skip (device-only)achswap init -p "your long passphrase"→ set it non-interactivelyachswap init --skip-passphrase→ device-only, no prompt (scripts)
A wallet is never created behind your back. If an AI client connects before
you have run init, the SDK refuses and tells the agent to ask you to run it.
That way you always see the recovery phrase and choose a passphrase. To opt into
the old silent behaviour: achswap set autoCreateWallet true.
achswap show-phrase — re-displays the 12-word phrase (it lives encrypted
inside keystore.json). Requires an interactive terminal and typing SHOW; it
has no scriptable flag on purpose. Use this if the phrase scrolled past you at
creation.
achswap delete-wallet — permanently removes keystore.json, vault.json,
and the device secret from the credential store. Requires typing DELETE. After
this the wallet only comes back from a recovery bundle or the 12-word phrase.
achswap passphrase set|remove|status — adds, changes, or removes the
passphrase by re-sealing the vault. Your address and key never change. Re-sealing
also rotates the device secret, so older copies of vault.json stop working.
achswap export-recovery <path> — writes a self-contained, passphrase-sealed
bundle that restores on any machine. This is your real disaster backup.
achswap import-recovery <path> — rebuilds keystore.json and a fresh local
vault from that bundle. Refuses to overwrite an existing keystore.
achswap recover — rebuilds from the 12-word phrase if everything else is lost.
achswap recover -m "word1 word2 … word12"
achswap migrate — one-time upgrade for wallets created before v1.1. See
Upgrading from an older SDK.
achswap backup [dir] — copies keystore.json + vault.json to another
folder. Only opens on the same machine and OS account — use export-recovery
for a backup that survives losing the device.
Quick reads (no unlock)
achswap address— prints your address without decrypting the keystore.achswap balance— prints your native USDC balance.
The interactive menu
Running achswap with no arguments opens a menu, so nothing has to be memorised:
[1] ◈ Pending transactions list · review · approve
[2] ⚙ Running processes list · kill
[3] ⚑ Settings view · edit · toggles
[4] 🔒 Wallet address · balance · init · recover
[5] ⇄ MCP Install Claude · Cursor · OpenCode · Codex
[6] ⚒ Tools run a tool · config · help
[7] ⛨ Security what protects this wallet
Settings ([3]) lists every option with its current value:
[ 1] automation trade (swaps auto · transfers ask)
[ 2] autoCreateWallet false
[ 3] mode local
...
automation shows what the level actually means, and flags itself when it is set
but not in effect — for example a passphrase-sealed vault with no
ACHSWAP_PASSPHRASE in the signer's environment:
[ 1] automation trade ⚠ inactive: This wallet has a passphrase,
which is not in the signer's environment.
Press w for a plain-English description of each setting, or r to reset back to
manual. Raising automation asks you to confirm first, and spells out that
full includes sending funds to any address.
Help (h) groups every command by what you are trying to do — first run,
everyday use, protecting the wallet, recovery, running the server — rather than
listing them alphabetically.
Status & troubleshooting
achswap security— shows your unlock factors, KDF parameters, credential store, and warns about single-factor setups, an automation level that is set but inactive, or a leftover.session-pw. Run this first if you are unsure how protected you are.achswap status— shows mode,automation, unlock mode,remoteUrl,rpcUrl,chainId, pending count, and any running server. Use it to confirm your setup (this is what printsmode: local, automation: manual, etc.).achswap config— prints the fully resolved config (env > file > defaults).achswap running(aliasps) — lists the Achswap MCP processes your client launched. If your AI client's tools don't appear, check here first.achswap running --kill 2— kill the process listed as #2achswap running --kill-all— kill them all (asks for confirmation unlessACHSWAP_YES=true)
achswap install <client>— writes the one-line MCP config into a client.<client>is one ofclaude | cursor | opencode | codex. It writes local mode so it works with no backend. Restart the client afterwards.
Configuration with set
achswap set <key> <value> changes a persisted setting. Valid keys:
| Key | Value | Effect |
|---|---|---|
automation | manual/trade/full | How much the agent may do alone (default manual) |
autoCreateWallet | true/false | Allow silent wallet creation on first use (default false) |
allowHeadlessKeyAccess | true/false | CI only — read key material with no terminal (default false) |
mode | local/remote | Where txs are built (local = on-device; remote = hosted Worker, not deployed yet) |
remoteUrl | URL | Hosted backend URL (remote mode) |
rpcUrl | URL | ARC RPC endpoint |
chainId | number | Chain ID (5042002 for ARC Testnet) |
builderToken | string | Optional Worker builder token |
Examples:
achswap set automation trade→ the agent trades on its own; sends still ask youachswap set automation manual→ the agent can only queue, never sendachswap set mode local→ build txs on-device (recommended; no backend needed)
After any set, restart the MCP server / your AI client for it to take effect.
Manual approval workflow
At automation: manual (the default), writes are queued instead of sent:
- The agent (or
achswap run) creates a pending transaction. achswap pendinglists them with an id.achswap approve <id>signs + broadcasts it from your terminal.
This is the human-in-the-loop path and the default: the agent can prepare trades, but only you can send them.
Running the server: serve and run
achswap serve starts the local MCP server. You normally never run this by
hand — your AI client launches it automatically via the install config
(type: local, command: npx -y @achswap/mcp-sdk serve). Options (advanced):
--http— run an HTTP server instead of stdio--port <port>— HTTP port (default8080)--host <host>— bind host (default127.0.0.1); use--insecure-bindto bind a LAN address (dangerous — anyone on the network could reach your signer)--single— refuse to start if another instance is already running
achswap run --tool <tool> --args <json> calls a single MCP tool and prints
the result, then exits. Use it to test the server or query the chain without an
AI client — and to convert amounts the right way:
achswap run --tool get_decimals --args '{"token_address":"USDC"}'→18achswap run --tool to_wei --args '{"token_address":"USDC","amount":"1.5"}'→1500000000000000000achswap run --tool get_native_balance --args '{}'
Tools (37)
The SDK exposes 37 tools to the agent. generate_wallet is hidden (the wallet
is created locally via achswap init). Amounts are always in base units (wei);
use to_wei to convert a human amount, and always call get_decimals first —
USDC and wUSDC are 18 decimals; other tokens vary (never assume 6).
Reads (no signing)
| Tool | Purpose |
|---|---|
get_wallet_address | Your wallet address (no args) |
get_wallet_info | Your wallet address / identity (no args) |
get_native_balance | Native USDC balance (18 decimals, gas token) |
get_token_balance | ERC-20 balance (proper decimals) |
get_all_token_balances | All ERC-20 balances |
get_allowance | Router/spender allowance |
get_token_info | Symbol + decimals for any token |
get_decimals | Decimals for any token (call before converting) |
to_wei | Human amount → wei using real decimals |
from_wei | wei → human amount using real decimals |
check_rpc_status | RPC / chain health |
get_pool_reserves | V2 pool reserves |
check_pair_exists | V2 pair lookup |
get_add_liquidity_ratio | Token-B amount for given Token-A |
get_swap_quote | V2 quote (output for input) |
get_swap_quote_reverse | V2 reverse quote (input for output) |
quote_adapter | Best route across V2 + V3 (no tx) |
get_liquidity_position | LP balance + underlying |
get_transaction_history | Recent txs for any wallet |
get_token_holders | Top holders + % of supply |
Writes (build unsigned tx → sign locally)
| Tool | Purpose |
|---|---|
transfer_token | ERC-20 transfer |
transfer_native | Native USDC transfer |
wrap_native | Wrap native USDC → wUSDC |
unwrap_wusdc | Unwrap wUSDC → native USDC |
approve_token | Approve spender (or max) |
swap_via_adapter | Any→any, auto-routed V2+V3 |
swap_native_via_adapter | Native USDC → ERC-20 |
swap_to_native_via_adapter | ERC-20 → native USDC |
add_liquidity | Add V2 liquidity (token + token) |
add_liquidity_eth | Add V2 liquidity (native + token) |
remove_liquidity | Remove V2 LP (both tokens) |
remove_liquidity_eth | Remove V2 LP (native + token) |
remove_liquidity_token | Remove V2 LP (wUSDC + token) |
deploy_token | Deploy ERC-20 (name, symbol, supply) |
burn_token | Burn tokens from balance |
SDK control (manual mode)
| Tool | Purpose |
|---|---|
confirm_transaction | Approve a queued pending tx (if allowed) |
list_pending | List queued txs awaiting approval |
Example agent workflow (USDC → ACHS)
1. get_decimals(token_address="ACHS") → 18
2. to_wei(token_address="USDC", amount="1.5") → 1500000000000000000
3. quote_adapter(token_in="USDC", token_out="ACHS", amount_in="1500000000000000000")
→ expected output + route
4. swap_via_adapter(token_in="USDC", token_out="ACHS", amount_in="1500000000000000000")
→ signed + broadcast (auto-sign), returns tx hash
Security & trust
- Private key never leaves your machine. It is created and stored only in
~/.achswap/keystore.json. The MCP server (hosted or local) only ever receives unsigned transactions. - The keystore password is 256-bit random, not something you or an attacker can guess, and it is never written to the Achswap folder.
- It is sealed in a vault whose key comes from your OS credential store and/or your passphrase — never from anything on disk. On Windows the vault is also DPAPI-bound to your logon session.
- A stolen
~/.achswapfolder is not enough. An attacker also needs your OS user session, and your passphrase if you set one. There is no low-entropy secret in the folder to attack offline. - Keys are never cached. Each signature unlocks, signs, and releases; a long-running MCP process holds no decrypted key between transactions.
- Manual approval by default.
automation: manual, so a compromised AI or MCP process cannot move funds on its own. - AI self-approval is doubly gated — it needs
ACHSWAP_ALLOW_AI_CONFIRM=trueand an explicit passphrase. Device unlock alone is never authorization. - Transactions are validated before signing. The SDK checks
chainId, that the destination matches the tool, and that the amount moved equals the amount requested — so a call for 10 cannot come back as a transaction for 10,000. - The pending queue is tamper-evident. It carries an HMAC keyed from the device secret, and approval refuses to sign a queue edited outside Achswap.
- Approval shows you the transaction — destination, amount, chain, calldata and expected effects — before asking you to confirm.
- No key in logs, errors, or tool output. Errors returned over MCP are scrubbed of long hex values, anything labelled password/passphrase/secret/mnemonic, and BIP39-looking word runs.
- Recoverable by you. Portable recovery bundle, 12-word phrase, or the local vault.
What this does not protect against
-
Live code execution as your logged-in user. Malware — or an AI coding agent with shell access — can ask the OS credential store for the device secret, exactly as the SDK does, then sign or run
achswap show-phrase. Device mode protects data at rest, not a live session compromise.achswap passphrase setis the fix: with a passphrase, code running as you still cannot sign or reveal the phrase, because that factor is only in your head.Note the boundary this does not cross: an agent restricted to the achswap MCP tools cannot extract key material at all. No tool returns a private key or mnemonic,
generate_walletis removed from the catalog, and errors are scrubbed. The exposure comes from shell access, not from the tool surface — prompt injection alone does not reach the key. -
A weak recovery-bundle passphrase. The bundle is deliberately not device-bound, so its passphrase is all that protects it. Make it long and keep the file offline.
-
A passphrase in the environment.
ACHSWAP_PASSPHRASEis readable by anything that can inspect the process environment. Prefer a short-lived shell, and never put it in an AI client config file. -
A leaked 12-word phrase. It reconstructs the wallet with no other factor. Treat it like the key itself.
Run achswap security to audit your own install.
AchSwap cannot move your funds: it has no key, only the ability to prepare transactions that your local signer approves.