# Why an AI agent should never hold a private key

> A key grants unlimited authority, and every limit on top is software. An appointment can be withdrawn; a copied key cannot be un-copied.

- Source: https://defiloops.com/blog/why-an-ai-agent-should-never-hold-a-key
- Published: 2026-07-21
- Category: Security
- Tags: agentic-wallet, ai-agents, key-management, custody
- Author: DeFiLoops

---
The fastest way to build an agentic wallet is to generate a private key, hand it to the
agent, and write some rules about what it may do with it. Almost every early build works
this way, and the industry is now arguing its way out of it — Coinbase has made the case
publicly, and the reasoning is short enough to fit in a sentence.

A private key is not a permission. It is *the* permission. Everything you layer on top is a
request.

## What a key actually grants

<Compare left="What the rules say" right="What the key grants" verdict>
  <Fragment slot="left">
    Spend up to 500 USDC a month. Only these three protocols. Never send to an address not
    on the list.
  </Fragment>
  <Fragment slot="right">
    Sign any transaction this account can make, for any amount, to any address, on any
    chain, for ever.
  </Fragment>
</Compare>

The left column is enforced by whatever code happens to be running between the agent and
the chain. The right column is enforced by mathematics. When those two disagree — because
of a bug, a compromise, a persuaded model, or a process that was restarted with the wrong
config — the right column wins every time.

<Callout type="danger" title="Rules over a key are a filter, not a boundary">
  A filter is evaluated against attacks somebody already imagined. It has to correctly
  refuse things it has never seen, under adversarial pressure, at machine speed. That is a
  hard job, and it is a job that only exists because the key made it necessary.
</Callout>

## The asymmetry nobody plans for

Here is the part that changes the architecture rather than the configuration.

**You cannot un-copy a key.**

If a key leaks — a log line, a memory dump, a compromised dependency, a backup that was not
supposed to exist — everyone who has a copy has full authority, permanently, and you cannot
tell how many copies there are. Rotating means moving every asset to a new address, which
is itself a set of transactions you now have to trust.

An appointment is the opposite. It lives on the chain, it is checked on every call, and it
can be withdrawn in one transaction that needs nobody's cooperation.

<Spec title="A leaked key against a revoked appointment" rows={[
  ['How many copies exist', 'Key: unknowable. Appointment: not a thing that can be copied'],
  ['How you stop it', 'Key: move every asset. Appointment: one transaction from your own wallet'],
  ['How fast it takes effect', 'Key: once the last asset has moved. Appointment: at the next call'],
  ['Whose help you need', 'Key: enough gas on every chain, and time. Appointment: nobody’s'],
  ['If the operator is hostile', 'Key: they may already have a copy. Appointment: they cannot grant themselves one'],
]} />

## What replaces the key

Three answers are in circulation, and they are not equivalent.

<Steps>
  <Step title="Split the key (MPC or multisig)">
    No single party can sign alone, and a policy engine decides what gets co-signed. This is
    the dominant commercial answer and it is a genuine improvement: there is no longer one
    object whose theft ends the story.

    What it does not change is expressiveness. The thing being co-signed is still an
    arbitrary transaction, so the policy engine is still a filter deciding whether a
    destination address is acceptable.
  </Step>
  <Step title="Keep the key away from the model">
    Put the signing key in hardware or a separate service the model cannot read, and let the
    model only request signatures. Better again — a persuaded model can no longer exfiltrate
    the key.

    But a persuaded model can still ask for the wrong thing, and something still has to
    judge the request.
  </Step>
  <Step title="Give the agent no key and no vocabulary for an address">
    The agent is a contract your account appoints. What it submits is a fixed form of
    labelled boxes drawn from a closed list of operations. There is no box for a destination
    and no box for a payload; destinations are roles, resolved from your account when the
    step runs.

    A persuaded model cannot express "send it to me", so nothing has to detect that it tried.
  </Step>
</Steps>

## What we actually hold

Worth being concrete, because "the agent is a contract" invites the question of what is
left on our side.

<Spec rows={[
  ['The agent', 'A contract. Your account appoints it. A plain key cannot be appointed at all'],
  ['The keeper', 'A key we hold. The only thing it can do is tell that contract to make one call'],
  ['The fee payer', 'Pays gas. Holds no assets, has no standing permission to spend anywhere'],
]} />

A stolen keeper key cannot take your account. The contract refuses on the chain, before the
call happens: it will not target your account directly, will not run the instructions that
install permissions or change ownership, and will not touch an operation you never switched
on. It is also bounded by caps fixed in your account that nothing on our side can raise.

<Callout type="warn" title="Where this is not airtight, stated plainly">
  The guard works as a list of banned actions rather than a list of permitted ones. It
  completely blocks a takeover — the agent can never become the owner or hand your account
  to somebody else — but it does not block every way value could move through an action
  nobody thought to ban. We have proven four such attacks against ourselves on a copy of
  the real Base network, and there has been no external audit.

  The service holding the keeper key also refuses to start in a production setting, on
  purpose, because it currently keeps that key in memory.
</Callout>

## The one-way door

One more property, because it is the other half of "an appointment can be withdrawn".

Your account can never be re-pointed at a *different* agent. The first appointment binds,
and the only permitted change is to none at all.

<Quote>
  An account whose agent can be swapped has a role in it — whoever can perform the swap.
  That role is a target, and its existence is the problem.
</Quote>

This is a real constraint on us, not a feature we enjoy. It means we cannot patch a
deployed agent underneath you. A new agent is something you move to deliberately, with a
new account. Slower for us; it is the point.

## How to evaluate any of this

You do not need to trust a security page. Ask what the agent can write down.

If the answer contains an address, every control that vendor lists is a filter standing
between a persuadable model and your money — and filters are judged against the attacks
somebody already thought of. If the answer does not contain an address, ask where the
spending cap is enforced and whether you can leave with their servers switched off.

Those two questions separate the designs faster than any amount of documentation.