Giving an AI Agent a Wallet, Safely
If an AI agent is going to move real value, it should only be able to move the value you decided it can move — on the actions you chose, to the places you allow, at a rate you set. The SDK’s agent session exists for exactly that.
The idea
A normal wallet session can do anything the key can do. An agent session wraps the same key with a policy that is checked before every action leaves the SDK:
const sdk = new XChainSDK({ network: 'dogecoin-testnet' });
const agent = sdk.agentSession(wif, {
// Nothing is allowed unless you list it.
allowedActions: ['SEND', 'EXECUTE', 'COINPAY'],
// Where funds may go (optional — omit to allow any destination).
allowedDestinations: ['DTqQ...storefront', 'DBgH...treasury'],
// The most a single action may move.
maxPerAction: { SEND: { MYTOKEN: '100', '*': '10' } }, // '*' = any other token
// The most the agent may move in total, per rolling window.
maxPerWindow: { hours: 24, perTick: { MYTOKEN: '500' }, maxActions: 50 },
// Above this, a human (or supervising process) must say yes.
confirmAbove: {
perTick: { '*': '50' },
handler: async (ctx) => await askTheOperator(ctx), // ctx: action, tick, amount, windowUsage
},
// See every denial (alerting, logs).
onPolicyViolation: (v) => console.error('agent blocked:', v.code, v.message),
});
await agent.send({ tick: 'MYTOKEN', amount: '5', destination: 'DTqQ...storefront' });
Every successful action’s result carries result.policy — what was checked and how much of the window is used — so the agent can reason about its own remaining budget instead of discovering limits by hitting them.
What happens on a violation
The action is refused before anything is signed or broadcast, with a typed SDKPolicyError whose code says exactly why (POLICY_ACTION_DENIED, POLICY_DESTINATION_DENIED, POLICY_AMOUNT_EXCEEDED, POLICY_WINDOW_AMOUNT_EXCEEDED, POLICY_WINDOW_COUNT_EXCEEDED, POLICY_CONFIRMATION_DENIED). Agents should treat these as final answers, not errors to retry.
Designed to fail closed
- No
allowedActionslist → the session refuses to construct. Nothing is allowed by default. - Window usage is persisted to disk (default
~/.xchain/agent-usage-<address>.json), so restarting the agent — or crash-looping it — does not reset the spending window. - If that usage file is corrupted, the session blocks rather than silently starting a fresh window. Delete it deliberately to reset.
- Amount checks use exact decimal arithmetic. There is no floating-point edge to slip through.
What this does — and does not — protect against
This is a guardrail around the agent, not around the key. It protects you from an agent that hallucinates an amount, gets prompt-injected into draining a wallet, or loops on a bad plan. It does not protect you from an attacker who steals the WIF itself — they can use the raw SDK without the policy.
Two practices close most of that gap:
- Fund the agent’s address like a spending account, not a vault. The window cap only has to be wrong by one top-up.
- For hard enforcement, the roadmap includes a MuSig2 co-signer: the agent holds one key, a policy daemon holds another, and the network sees a single aggregate signature that simply cannot be produced outside policy. The SDK already ships MuSig2 (BIP-327); the co-signer service interface is specified and planned.
For MCP users
The xchain-mcp server’s read tools never touch keys. Its write tool (submit_action) stays off until the operator configures a key and policy, and when on it routes every submission through an agent session — there is deliberately no unpoliced write path for agents. See the MCP Quickstart.