@moneolabs/wallet
An account per AI agent, with keys the model never sees.
npm install @moneolabs/wallet
Agents leak. They paste their context into logs, into tools, into other models. So this package is
built on the assumption that anything the agent can read is already public: the wallet holds a
handle to a signer, and the signing happens somewhere the agent cannot reach.
Use
import { createWallet, localSigner, memoryRail } from "@moneolabs/wallet";
const wallet = await createWallet({
agent: "research-agent-01",
signer: localSigner(),
rail: memoryRail(),
asset: "USD",
funding: "$500",
});
wallet.address;
await wallet.balance();
await wallet.pay({ to: "x402:api.pricefeed.dev/quote", amount: "$0.04" });
await wallet.sweep({ to: "treasury", leave: "$50" });
There is no field anywhere on the wallet that returns key material.
Attach a policy
A wallet with no limits is a liability. Pass a guard and every payment is checked before it is
signed.
import { createGuard } from "@moneolabs/guard";
const guard = createGuard({
perTransaction: { max: "$25" },
rolling24h: { max: "$100" },
counterparties: "allowlist-only",
allow: ["x402:*"],
});
const wallet = await createWallet({ agent: "a", signer, rail, guard, funding: "$500" });
await wallet.pay({ to: "0xstranger", amount: "$5" });
Ask first without paying:
const decision = await wallet.preflight({ to: "vendor:acme", amount: "$400" });
decision?.verdict;
decision?.reason;
balance() is what the rail holds. available() subtracts anything the guard has reserved but not
yet settled, which is usually the number you want before promising to spend.
Custody
Signer is an interface. Two implementations ship:
localSigner();
localSigner({ seed });
remoteSigner({ address, sign });
remoteSigner sends a digest out and gets a signature back. Nothing about the key crosses into
your process, so there is nothing in it worth stealing.
const signer = remoteSigner({
address: "0x4a91...c7d2",
custody: "tee",
async sign(digest) {
const res = await fetch("https://enclave.internal/sign", {
method: "POST",
body: JSON.stringify({ digest }),
});
return res.json();
},
});
Rails
Rail is where value actually moves: a chain, a card processor, an internal ledger. memoryRail
is a complete double-entry ledger that happens to live in memory. Balances go down on the sender
and up on the receiver, overdrafts are refused, and every transfer gets a reference.
const rail = memoryRail({
fee: (amount) => scaleMoney(amount, 0.001),
balances: { treasury: { USD: money("10000", "USD") } },
});
Implement transfer, balance, and optionally credit to connect a real one. Everything above the
interface stays the same.
Agent toolkit
Tool definitions in the shapes model APIs expect, with handlers wired to the wallet.
import { toolkit } from "@moneolabs/wallet";
const tools = toolkit(wallet, { guard });
tools.anthropic;
tools.openai;
await tools.handle("pay", { to: "x402:api.dev", amount: "$0.04" });
Five tools: get_balance, preflight_payment, pay, get_spending_limits, list_payments.
Restrict them with include.
The important detail is what a refusal looks like:
{
"ok": false,
"refused": true,
"reason": "$900.00 exceeds the $25.00 per-transaction limit",
"rule": "perTransaction",
"hint": "This limit is set by the wallet owner. Ask them to raise it rather than retrying."
}
A result, not an exception. A model that receives a stack trace retries until the loop gives up. A
model that receives this stops and tells the user.
Failure handling
If the rail refuses or the signer fails, the guard reservation is released rather than left stuck.
Budgets do not leak from failed payments.
await wallet.pay({ to: "v", amount: "$50" });
(await guard.usage()).budgets[0].used;
License
MIT