Agent Permissions: The Need for Scoped Access, Not Private Keys

By Joan Alavedra, Co-Founder at Openfort9 min read
Agent Permissions: The Need for Scoped Access, Not Private Keys

AI agents have a key problem. Not a key-management problem — a private-key problem. The whole crypto stack assumes one root primitive: a 32-byte secret that signs anything you put in front of it. That primitive was designed for humans guarding their own funds. It is the wrong primitive for software making automated decisions.

Agents do not need private keys. They need permissions.

For a deeper read on the custody side of this question, see The Agentic Wallet Problem. This post is about the layer above it — the rule layer that makes scoped agent access actually work.

What Agent Permissions Are

An agent permission is a rule that limits what a delegated key can do on a smart account. Instead of "this key can sign anything," the permission says "this key can spend up to 100 USDC per day, only against the Uniswap V3 router, only in the next 24 hours, and only the exactInputSingle function."

A few rules together make up an agent permission wallet — a smart account where the agent never holds the root key. The owner keeps the admin key. The agent gets a registered, scoped, expiring, revocable signing key. Every transaction the agent submits is validated against the rules before it executes.

The model has four moving parts:

PartRole
Owner keyFull authority. Registers and revokes session keys. Updates policy. Holds the kill switch.
Session key (non-admin)The key the agent actually signs with. Constrained by policy. Has an expiration.
PolicyThe rules: spending caps, contract allowlists, function filters, time windows, rate limits.
Enforcement layerWhere the rules run — on-chain in the smart account, in a TEE bound to the signer, or in an off-chain policy engine.

The agent never sees the owner key. It signs with the session key, the smart account checks the policy, and the transaction either executes or reverts.

Why "Just Use a Private Key" Fails for Agents

Three reasons.

1. Private keys are unconditional. An EOA private key signs whatever hash it is given. There is no concept of "this key is allowed to spend X but not Y." You can layer checks in your backend, but those checks live next to the key — anyone who can read the key can bypass them. A leaked .env is a drained account.

2. Agents make decisions you cannot fully audit. An LLM-driven agent takes a prompt, reasons about it, and emits a transaction request. Prompts can be poisoned. Tool outputs can be manipulated. Models hallucinate. None of that should be able to drain your wallet, and at the EOA layer, all of it can.

3. You need to revoke without redeploying. If the agent goes rogue, you want to kill its access in one transaction without rotating the root key, recovering custody, or migrating users. EOA keys do not support that. Smart account session keys do.

The fix is a level of indirection. The owner stays an admin. The agent operates a key that the smart account treats as conditional. The conditions are the permission set.

What a Real Agent Permission System Must Enforce

Pick any of these rules from a checklist and any decent permission system covers them; the differentiator is whether they are enforced at signing time or just suggested:

  • Time bounds. A validAfter and validUntil timestamp. The signature is invalid outside the window.
  • Usage quotas. How many transactions the key can submit before it stops working. Useful for one-shot delegations.
  • Spending caps. Native and ERC-20 caps, ideally per-period (per minute, hour, day, week, month). Periods reset automatically.
  • Contract allowlists. The set of target contracts the key can call. Wildcards allowed (ANY_TARGET) but only with strict caps.
  • Function filters. The 4-byte selectors the key can invoke on each allowed contract. Stops "approve everything" attacks against an allowlisted token.
  • Argument predicates. For high-risk calls, validate parameters — e.g. block transfers to addresses outside an allowlist, or cap the amount argument.
  • Rate limits. Maximum executions per time window. Stops a misbehaving agent from draining the cap in seconds.

For the deep dive on what each of these looks like under the hood — including period semantics, wildcards, and how Openfort packs all of this into a uint256 per key — see Technical Dive: Key Permissions for Accounts.

How Openfort Implements Agent Permissions

Openfort smart accounts implement these rules natively. Every key registered on the account has a settings word, an expiration, and an optional hook contract for custom validation logic. The relevant parts of the permissions reference in plain terms:


_48
// Granting a session key with scoped permissions
_48
import { Openfort } from '@openfort/openfort-js'
_48
_48
const openfort = new Openfort({
_48
baseConfiguration: { publishableKey: process.env.OPENFORT_PUBLISHABLE_KEY }
_48
})
_48
_48
const sessionKey = await openfort.embeddedWallet.registerSessionKey({
_48
// The agent's signing address
_48
address: agentAddress,
_48
_48
// Time window: 24 hours from now
_48
validAfter: Math.floor(Date.now() / 1000),
_48
validUntil: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
_48
_48
// Non-admin: cannot register other keys or change account settings
_48
isAdmin: false,
_48
_48
// Permission policy enforced by the smart account
_48
policy: {
_48
// Native spend cap: 0.05 ETH per day
_48
nativeLimit: { amount: '50000000000000000', period: 'DAY' },
_48
_48
// ERC-20 spend caps
_48
tokenLimits: [
_48
{
_48
token: USDC_ADDRESS,
_48
amount: '100000000', // 100 USDC (6 decimals)
_48
period: 'DAY',
_48
},
_48
],
_48
_48
// Contract allowlist with function filters
_48
callPolicies: [
_48
{
_48
target: UNISWAP_V3_ROUTER,
_48
selectors: [EXACT_INPUT_SINGLE_SELECTOR],
_48
},
_48
{
_48
target: USDC_ADDRESS,
_48
selectors: [APPROVE_SELECTOR, TRANSFER_SELECTOR],
_48
},
_48
],
_48
_48
// Rate limit: max 10 transactions per hour
_48
rateLimits: [{ count: 10, period: 'HOUR' }],
_48
},
_48
})

The agent then signs UserOperations with this key. The smart account validates each one against the policy before executing it. A transaction that violates any rule reverts at validation time and never lands.

When you want to kill the key:


_10
await openfort.embeddedWallet.revokeSessionKey({ keyId: sessionKey.id })

One transaction, on-chain, immediate. No code deploy, no key rotation, no user migration.

For a full working example — frontend, backend, agent, and smart account wired together — see the agent-permissions recipe and the How to Build Wallet Permissions walkthrough. It runs a backend agent that executes DCA trades on a 5-minute session key, then expires.

Where the Rules Run: On-Chain, Off-Chain, or TEE

Three places to enforce a policy. Each has different trust assumptions.

LayerMechanismStrengthWeakness
On-chainSmart account validates every UserOp against the registered policy.Trustless. Verifiable. Survives any backend going down.Higher gas. Policy changes require on-chain transactions.
TEEKey lives in a Trusted Execution Environment. Policy is part of the signing path.Hardware-level isolation. Key cannot leave the enclave. Fast.Trust the TEE attestation chain. Policy is private to the operator.
Off-chainPolicy engine in your backend approves or rejects requests before signing.Flexible. Easy to iterate. Can use ML for fraud detection.Custodial-shaped. Every call site must hit the engine. Operator can change rules silently.

For most agent products the right answer is on-chain rules + TEE-bound key. The on-chain layer gives users and auditors a trustless guarantee about what the agent can do. The TEE binds the signing key to the path where those rules are evaluated, so the key cannot be exported and used outside the agent's intended context. Openfort's backend wallets are TEE-bound (Google Cloud TEE), and the smart account layer is on-chain — see How to Build an Agent Wallet for the end-to-end setup.

Common Mistakes When Granting Agent Permissions

Things we see go wrong in production:

  • No expiration. "This key is for the agent, the agent is always running, we'll set it to never expire." Then the project pivots and the key is still live a year later. Always set validUntil. If the agent needs to keep going, rotate.
  • approve(MAX_UINT256) against the whole router. A spending cap on the smart account does nothing if the agent has approved a router for unlimited USDC and the router can pull. Either keep approvals scoped per-call or include the approve target in the function-level allowlist with a value cap.
  • Granting admin to the agent. Admin keys can register other keys. An admin agent can mint itself permanent access. Agents should always be isAdmin: false.
  • No kill switch path. Owner keys lost in an unrecoverable wallet means no revocation. Use a recovery scheme (passkey + multisig, or social recovery) on the owner side. The owner should be the human or operator, not a service account.
  • Off-chain policy with no on-chain backstop. "We check spending limits in the API." Anyone who calls the signing primitive directly bypasses your check. If a policy matters, encode it in the smart account.

When Agent Permissions Are Not Enough

Permissions cover scope, not intent. They will stop an agent from spending more than 100 USDC a day. They will not stop an agent from spending exactly 100 USDC a day on something useless. For that you need a layer above — human approval flows for transactions over a threshold, or a separate review agent that evaluates outgoing transactions before they sign.

Permissions also do not solve key management for the owner. The owner key still needs to be safe — passkeys, MFA, hardware-bound. If the owner is compromised, the agent's permissions are too. Treat the owner key like the master key it is, even when you are mostly thinking about delegated agent flows.

Where to Go from Here

If you are building an agent that has to spend, the path is:

  1. Read the architecture. The Agentic Wallet Problem covers the custody trilemma and the Owner-Operator key model.
  2. Pick a stack. Best Agent Wallets for Developers compares the eight or so platforms in the agent wallet space, including how each one handles the policy layer.
  3. Understand the primitives. Technical Dive: Key Permissions for Accounts goes deep on what you can encode at the smart account level.
  4. Build. How to Build Wallet Permissions walks through a working DCA agent on Openfort. How to Build an Agent Wallet covers the backend wallet path with EIP-7702.

Or skip ahead and start in the Openfort dashboard — session keys, on-chain policies, and TEE-bound backend wallets are available on the free tier. The docs cover the SDK, and the recipes-hub has end-to-end agent permission examples you can clone in a minute.

The future of agent infrastructure is not "give the bot a wallet." It is "give the bot a permission, and let the wallet enforce it."

Share this article

Related Articles

  1. How to Migrate from Alchemy AccountKit

    Step-by-step guide to migrate your React app from Alchemy Account Kit to Openfort embedded wallets after Alchemy's signer sunset.

  2. Best Privacy Apps in 2026

    A curated lineup of consumer-facing privacy apps worth knowing in 2026, with a short feature spotlight on each.

  3. Best Stablecoin Apps in 2026

    A curated lineup of consumer-facing stablecoin apps worth knowing in 2026, with a short feature spotlight on each.