Agent-safe signing · Devnet · ~10 minutes

Give an AI agent a Solana wallet it cannot drain

Most agents run with a private key in an environment variable. This walks through the alternative: a session key bounded by a policy the root key signed, connected to Claude or Cursor over MCP, or dropped into Solana Agent Kit. The agent proposes; the policy decides; the root never comes online.

Before you start. Coldstar is beta software and this quickstart is devnet-only. The policy engine and signing core are in scope for a planned independent audit. Read the threat model before you put anything of value behind it.

1Install

npm install coldstar-agent-signer @solana/web3.js tweetnacl

The package is MIT-licensed and published from github.com/ExpertVagabond/coldstar-agent-signer. It exports ColdstarWallet, the policy engine, and two commands: coldstar-sign-policy for the cold side and coldstar-signer-mcp for the agent side.

2Write the policy

A short JSON file. Rules are evaluated in this order, first match wins: blocklist, program allowlist, escalate threshold, per-transaction limit, recipient allowlist, daily cap, then auto-sign.

{
  "version": 1,
  "limits": { "perTxSol": 0.05, "dailySol": 0.2 },
  "allowPrograms": [
    "11111111111111111111111111111111",              // System program (SOL transfers)
    "ComputeBudget111111111111111111111111111111"   // most SDKs prepend a priority-fee instruction
  ],
  "allowRecipients": ["<address the agent may pay>"],
  "allowTokens": ["SOL"],
  "blockRecipients": ["<address that must never receive funds>"],
  "escalateAboveSol": 0.05
}

Paying in USDC? List the mint in allowTokens and give it limits in tokenLimits, in base units (USDC has six decimals, so "25000000" is 25 USDC). The SOL limits do not bound tokens, because a token transfer moves no lamports. Coldstar refuses token instructions it cannot bound, Approve most of all: a delegate can drain the account later without signing anything else.

  "allowTokens": ["SOL", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"],   // SOL + USDC
  "tokenLimits": {
    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { "perTx": "25000000", "daily": "100000000" }
  }

Keep the program allowlist short. A transfer through an allowlisted program such as Jupiter cannot be statically decoded to an amount, so allowlisting a program means trusting what it does internally. Simulation-based accounting is available opt-in (COLDSTAR_SIMULATE=1) and tightens this; it does not remove it.

3Have the root sign it (cold side)

This is the step that makes the model real. The root key signs the policy for one session public key, with an expiry. Run it on the offline machine; the output is plain JSON that crosses the gap by file or QR. The root is not needed again until the policy changes or the grant expires.

# once: encrypt the root, then destroy the plaintext copy
coldstar-encrypt-key --in root.json --out /media/cold/root.coldstar.json && rm -P root.json

# each grant, on the air-gapped machine; the key file never leaves it
coldstar-sign-policy --root /media/cold/root.coldstar.json --policy coldstar.policy.json \
  --session <session public key> --expires 7d \
  --revoker <hot key that may cancel this grant> > envelope.json

The root is stored as Argon2id plus AES-256-GCM, the same container the Coldstar wallet uses, so signing asks for your passphrase. A plaintext key file still works and warns every time. Lose the passphrase and the key is gone, which is the design rather than a gap in it.

On the online host, the signer verifies the signature, that the envelope names the session key it holds, and the expiry, and refuses to start otherwise. Pin the root's public key too; without it, any key could have "signed" the policy.

Trying it on one laptop? Generate a throwaway root with solana-keygen new -o root.json and encrypt it as above, or run the Solana Agent Kit starter, whose npm run sign-policy does this with a clearly labelled demo root.

4Connect the agent

Claude Desktop · Claude Code · Cursor (MCP)

Add the server to your MCP client's config. The model gets five tools and never a key: status, verdict, sign, sign-and-send, and transfer SOL. Every outcome comes back as data (signed, escalated, rejected, send_failed) so the model reads the reason and stops instead of retrying.

{
  "mcpServers": {
    "coldstar": {
      "command": "npx",
      "args": ["-y", "-p", "coldstar-agent-signer", "coldstar-signer-mcp"],
      "env": {
        "RPC_URL": "https://api.devnet.solana.com",
        "COLDSTAR_POLICY": "/abs/path/envelope.json",
        "COLDSTAR_ROOT_PUBKEY": "<root public key>",
        "COLDSTAR_REQUIRE_ENVELOPE": "1",
        "COLDSTAR_SESSION_KEYFILE": "/abs/path/session.json",
        "COLDSTAR_LEDGER": "/abs/path/ledger.json",
        "COLDSTAR_CHAIN_LEDGER": "1",        // daily cap read back from the chain
        "COLDSTAR_CHECK_REVOCATION": "1"    // honour an on-chain kill switch
      }
    }
  }
}
Solana Agent Kit

ColdstarWallet implements the kit's BaseWallet, so it replaces KeypairWallet and every plugin keeps working. Use the kit's signOnly mode and broadcast yourself; the kit's default send path signs each transaction twice, which a metering wallet correctly counts twice.

import { SolanaAgentKit } from "solana-agent-kit";
import TokenPlugin from "@solana-agent-kit/plugin-token";
import { ColdstarWallet, FileSpendLedger } from "coldstar-agent-signer";

const wallet = ColdstarWallet.fromEnvelope({
  envelope: JSON.parse(fs.readFileSync("envelope.json", "utf8")),
  expectedRoot: process.env.COLDSTAR_ROOT_PUBKEY,
  session,                                        // the Keypair the root authorised; disposable
  rpcUrl: RPC_URL,
  ledger: new FileSpendLedger(".coldstar-ledger.json"),  // daily cap survives restarts
  onEscalate: async (tx, reason) => null,        // hand tx to the cold device; return it signed, or null
});
const agent = new SolanaAgentKit(wallet, RPC_URL, { signOnly: true }).use(TokenPlugin);

5Watch the three outcomes

AUTO_SIGN

Under the per-transaction limit and daily cap, to an allowlisted recipient through an allowlisted program. The session key signs. Latency is a local policy check.

ESCALATE

Over the threshold, or to an address the policy has not seen. The unsigned transaction goes to a human on the offline device as a QR code. Nothing online can produce that signature.

REJECT

Blocklisted recipient. The prompt-injected agent asking to send everything to an attacker gets a policy error. No signature is ever produced.

Ask the agent to send a small amount to the allowed address and it will. Ask it to send everything to the blocked address and watch the tool call fail closed. That second request is the whole reason this exists.

6Keep a kill switch

A grant expires on its own, but sometimes you want one dead now. Revocation is published on Solana, where nobody with access to the agent's machine can quietly delete it: the authority writes a signed marker naming the session key, and the signer reads the chain before every decision.

coldstar-revoke --authority revoker.json --session <session public key>

The --revoker key from step 3 can do this and nothing else, so an emergency stop does not mean opening the safe. Signers already running notice within a minute. A revoked grant is a hard refusal; a chain the signer cannot reach is an escalation, so cutting it off from RPC stops the agent rather than freeing it.

Be clear about what this stops. It stops a compromised agent, which has to go through the signer. It does not stop someone who has stolen the session secret key — they do not need this package at all, and no off-chain control can reach them. For that you need the funds held behind an on-chain program that enforces membership itself, which on Solana today means Squads spending limits, layered under Coldstar.

Frequently asked questions

Does the agent ever see a private key?

No. The agent holds a session key bounded by a policy the root signed; the root stays offline and only ever signs the policy. Through MCP the model does not even see the session key, only signed transactions or a refusal.

What happens when the agent tries to pay an address that is not allowed?

An address missing from the allowlist escalates to a human on the cold device. An address on the blocklist is rejected with no signature.

Can I cancel an agent's permission early?

Yes. coldstar-revoke publishes a signed marker on Solana naming the session key; the signer checks the chain before every decision and refuses everything once the grant is revoked. A hot revoker key does this without touching the root.

Is this ready for mainnet?

Not yet. Beta, pre-audit. Devnet only, short program allowlist, read the posture note.

How does this compare to Turnkey, Privy, or Coinbase's agent wallets?

Those keep the key in a vendor's enclave and evaluate policy there. Coldstar keeps the root on your own offline drive and evaluates policy on your machine. Each has real trade-offs; we wrote them up honestly in the agent-wallet comparison.

Read the code first

Security software you cannot read is a promise. This one is 100 tests and about a thousand lines you can.

GitHubnpmWhy this design