June 16, 2026 · updated June 18, 2026

Wallet-Enabled Agents Need Deployment Profiles

A deployment profile is a signed, machine-readable policy bundle for a wallet-enabled agent. It says what the agent may do, under what limits, through which tools, with what approval rules, and what audit records must be produced.


This is not a Coinbase security vulnerability report. I reviewed public code only, did not run mainnet transactions, and am not claiming that Coinbase has endorsed this analysis. I chose AgentKit because it is a clean example of an increasingly important problem: agents can now assemble wallet-backed capabilities faster than users and developers can assemble responsibility boundaries around them.

Definition

A wallet-enabled agent has two separate surfaces:

A deployment profile binds those surfaces together.

type DeploymentProfile = {
  profileId: string;
  agentId: string;
  wallet: WalletScope;
  runtime: RuntimeScope;
  authorities: Authority[];
  policies: ActionPolicy[];
  receiptSink: ReceiptSink;
  revocation: RevocationPolicy;
  signatures: ProfileSignature[];
};

The profile is not a prompt. It is not a disclaimer. It is a policy object used by the runtime before wallet actions execute.

Minimum Schema

A useful first version can be small. It needs wallet scope, runtime scope, action policy, evaluation rules, receipts, and revocation.

type WalletScope = {
  chainIds: number[];
  walletAddress: `0x${string}`;
  allowedAssets?: TokenRef[];
  deniedAssets?: TokenRef[];
  maxTotalValueUsd?: string;
};

type RuntimeScope = {
  framework: string;
  version?: string;
  allowedTools: string[];
  allowedHosts?: string[];
  environment: "local" | "hosted" | "tee" | "unknown";
};

type ActionRisk =
  | "read"
  | "low_value_write"
  | "value_transfer"
  | "allowance"
  | "trade"
  | "recurring_permission"
  | "external_payment";

type ActionPolicy = {
  action: string;
  risk: ActionRisk;
  chains?: number[];
  assets?: TokenRef[];
  counterparties?: AddressOrHost[];
  maxValueUsd?: string;
  maxPerDayUsd?: string;
  maxAllowanceUsd?: string;
  approvalTtlSeconds?: number;
  requiresHuman?: boolean;
  emitReceipt: boolean;
};

The key property is that high-impact actions carry explicit policy metadata. A host should not have to infer sensitivity from action names or natural-language descriptions.

Action Evaluation

Every proposed wallet action should be converted into a normalized action object before execution.

type ProposedWalletAction = {
  action: string;
  tool: string;
  chainId: number;
  valueUsd?: string;
  asset?: TokenRef;
  recipient?: `0x${string}`;
  spender?: `0x${string}`;
  allowanceUsd?: string;
  host?: string;
  calldata?: `0x${string}`;
  createsDurableAuthority: boolean;
};

type RuntimeState = {
  spentTodayUsd: string;
  openAllowances: Allowance[];
  priorReceipts: ReceiptRef[];
};

type Decision =
  | { type: "allow" }
  | { type: "deny"; reason: string }
  | { type: "require_human"; reason: string };

function evaluateWalletAction(
  profile: DeploymentProfile,
  action: ProposedWalletAction,
  state: RuntimeState
): Decision {
  if (!profile.runtime.allowedTools.includes(action.tool)) {
    return { type: "deny", reason: "tool_not_allowed" };
  }

  const policy = findPolicy(profile, action);
  if (!policy) {
    return { type: "deny", reason: "no_matching_policy" };
  }

  if (!profile.wallet.chainIds.includes(action.chainId)) {
    return { type: "deny", reason: "chain_not_allowed" };
  }

  if (exceedsValueLimit(policy, action)) {
    return { type: "require_human", reason: "value_limit_exceeded" };
  }

  if (exceedsDailyLimit(policy, action, state)) {
    return { type: "require_human", reason: "daily_limit_exceeded" };
  }

  if (action.createsDurableAuthority && policy.requiresHuman !== false) {
    return { type: "require_human", reason: "durable_authority" };
  }

  if (!matchesCounterparty(policy, action)) {
    return { type: "deny", reason: "counterparty_not_allowed" };
  }

  return policy.requiresHuman
    ? { type: "require_human", reason: "policy_requires_human" }
    : { type: "allow" };
}

This is the control point. Wallet signing should happen after policy evaluation, not before it.

Example Profiles

A read-only profile should reject all wallet writes.

const readOnlyProfile: DeploymentProfile = {
  profileId: "read-only-v1",
  agentId: "portfolio-assistant",
  wallet: {
    chainIds: [1, 8453],
    walletAddress: "0xabc..."
  },
  runtime: {
    framework: "agentkit",
    allowedTools: ["wallet.balance", "erc20.balanceOf"],
    environment: "hosted"
  },
  authorities: [],
  policies: [
    {
      action: "*",
      risk: "read",
      requiresHuman: false,
      emitReceipt: true
    }
  ],
  receiptSink: { type: "local_file", path: "./receipts.ndjson" },
  revocation: { type: "disable_profile" },
  signatures: []
};

A capped payment profile can allow small autonomous x402 payments while requiring approval above the cap.

const cappedX402Profile = paymentsWithCapProfile({
  chainIds: [8453],
  asset: { symbol: "USDC", address: "0x..." },
  maxPerRequestUsd: "0.25",
  maxPerDayUsd: "5.00",
  allowedHosts: ["api.example.com", "research.example.org"],
  receiptSink: { type: "https", url: "https://receipts.example.org/ingest" }
});

A mainnet trading profile should treat the swap and the approval as separate governance events.

const mainnetTradingProfile: DeploymentProfile = {
  profileId: "mainnet-trading-human-approval-v1",
  agentId: "trading-agent",
  wallet: {
    chainIds: [1, 8453],
    walletAddress: "0xabc...",
    allowedAssets: [
      { symbol: "USDC", chainId: 8453 },
      { symbol: "ETH", chainId: 1 }
    ],
    maxTotalValueUsd: "1000"
  },
  runtime: {
    framework: "agentkit",
    allowedTools: ["zeroX.swap", "erc20.approve"],
    environment: "tee"
  },
  authorities: [],
  policies: [
    {
      action: "zeroX.swap",
      risk: "trade",
      maxValueUsd: "100",
      maxPerDayUsd: "300",
      requiresHuman: true,
      emitReceipt: true
    },
    {
      action: "erc20.approve",
      risk: "allowance",
      maxAllowanceUsd: "100",
      approvalTtlSeconds: 86400,
      requiresHuman: true,
      emitReceipt: true
    }
  ],
  receiptSink: { type: "local_file", path: "./receipts.ndjson" },
  revocation: { type: "revoke_allowances_and_disable" },
  signatures: []
};

Receipt Format

Receipts make later review possible. A receipt should record the proposed action, the matched policy, the decision, the signer, and the resulting transaction or denial.

type WalletActionReceipt = {
  receiptId: string;
  profileId: string;
  agentId: string;
  action: ProposedWalletAction;
  matchedPolicy?: ActionPolicy;
  decision: Decision;
  humanApproval?: {
    approvedBy: string;
    approvedAt: string;
  };
  txHash?: `0x${string}`;
  createdAt: string;
  profileSignature?: string;
};

A profile without receipts is only a preflight control. A profile with receipts becomes an audit boundary.

AgentKit Case Study

I tested this proposal against public code in Coinbase AgentKit, at commit 0fe026b.

AgentKit already has useful primitives:

The missing piece is not basic guardrails. The missing piece is a shared policy envelope across transfers, approvals, swaps, spend permissions, and paid HTTP requests.

Permit2 Example

The 0x swap action shows why this matters. The action description says a swap may automatically approve the Permit2 contract to spend the sell token. When allowance is needed, the implementation sends an ERC-20 approve(PERMIT2_ADDRESS, maxUint256) before fetching and executing the final quote. See the action description and the approval code.

This may be normal for Permit2 workflows. The issue is not that the code is wrong. The issue is that the approval and the swap are separate governance events.

A deployment profile should force that separation:

const approveDecision = evaluateWalletAction(profile, {
  action: "erc20.approve",
  tool: "erc20.approve",
  chainId: 8453,
  asset: { symbol: "USDC", chainId: 8453 },
  spender: PERMIT2_ADDRESS,
  allowanceUsd: "unbounded",
  createsDurableAuthority: true
}, state);

// Expected result:
// { type: "require_human", reason: "durable_authority" }

The user may approve the final trade. That does not automatically mean the user approved an unbounded durable allowance.

Required Profile Modes

A framework does not need one universal policy. It needs named modes with explicit tradeoffs.

The goal is not to make every agent timid. The goal is to make autonomy named, scoped, enforceable, and reviewable.

Disclaimers Are Not Controls

The AgentKit README includes a legal and privacy disclaimer saying the software is experimental, that agent acts are not acts of Coinbase, and that users are responsible for evaluating output and use cases. See the README language.

That disclaimer is reasonable. It does not close the control loop.

If users are responsible, they need operational controls: preflight summaries, caps, approval queues, receipts, and revocation paths. Otherwise responsibility is assigned in text but not supported in the system.

Implementation Path

A practical 30-day implementation path:

  1. Add risk metadata to TypeScript and Python action interfaces without changing behavior.
  2. Add a policy evaluator that can return allow, deny, or require_human.
  3. Add default deployment profiles for read-only, testnet, capped payments, mainnet human approval, and bounded autonomy.
  4. Update templates so developers choose a deployment profile during setup.
  5. Emit receipts for all value movement, all approvals, all paid API calls, and all denials.
  6. Ship a revocation helper that disables the profile and revokes known token allowances where possible.

The high-value question for a wallet-enabled agent framework is not "is this framework safe?" That question is too broad. The sharper question is:

Which deployment profiles should the framework officially support for agents that can move value?

That is a product question, a developer-experience question, and a trust question.

The Work I Am Offering

I am offering small autonomy and governance audits for agentic architectures: public docs plus limited supplied context, a source-grounded report, a Critic appendix, and three recommendations ranked by cost and impact. The beta price is $500 USDC.

The deliverable is a map of where an agent's capability surface exceeds its authority surface, plus concrete profile recommendations.