Agent Account

The Agent Account is a smart contract account designed specifically for AI agents. It extends Starknet's native account abstraction with session keys, spending limits, timelocked upgrades, and ERC-8004 identity integration.

Overview

The Agent Account provides:

  • Session keys - Temporary keys with limited permissions and spending caps
  • Spending limits - 24-hour rolling caps per token per session key
  • Contract restrictions - Whitelist which contracts a session can call
  • Time bounds - Sessions automatically expire
  • Timelocked upgrades - Schedule contract upgrades with configurable delay
  • Emergency revocation - Owner can instantly revoke all session keys
  • ERC-8004 integration - Bind on-chain identity to the account

Why Agent Accounts?

Standard EOA (Externally Owned Account) wallets aren't suitable for AI agents because:

ProblemAgent Account Solution
Private key exposureSession keys with limited scope
Unlimited spendingPer-token 24-hour spending limits
No contract restrictionsWhitelist of allowed contract calls
No expirationTime-bounded sessions
Instant upgradesTimelocked upgrades with owner delay

Data Structures

SessionPolicy

Defines the constraints for a session key.

pub struct SessionPolicy {
    pub valid_after: u64,           // Unix timestamp when session becomes valid
    pub valid_until: u64,           // Unix timestamp when session expires
    pub allowed_contract: ContractAddress, // Zero = any contract allowed
    pub spending_token: ContractAddress,   // Token for spending limit (zero = no limit)
    pub spending_limit: u256,       // Max spend per 24-hour period
}

Events

SessionKeyRegistered

Emitted when a session key is registered.

pub struct SessionKeyRegistered {
    #[key]
    pub session_public_key: felt252,
    pub valid_after: u64,
    pub valid_until: u64,
}

SessionKeyRevoked

Emitted when a session key is revoked.

pub struct SessionKeyRevoked {
    #[key]
    pub session_public_key: felt252,
}

UpgradeScheduled

Emitted when an upgrade is scheduled.

pub struct UpgradeScheduled {
    pub new_class_hash: ClassHash,
    pub scheduled_at: u64,
    pub execute_after: u64,
}

AgentIdSet

Emitted when the agent identity is bound.

pub struct AgentIdSet {
    pub registry: ContractAddress,
    pub agent_id: u256,
}

Interface

IAgentAccount

#[starknet::interface]
pub trait IAgentAccount<TContractState> {
    // Session key management
    fn register_session_key(ref self: TContractState, key: felt252, policy: SessionPolicy);
    fn revoke_session_key(ref self: TContractState, key: felt252);
    fn emergency_revoke_all(ref self: TContractState);
    fn is_session_key_valid(self: @TContractState, key: felt252) -> bool;
    fn get_session_policy(self: @TContractState, key: felt252) -> SessionPolicy;
    fn get_session_key_count(self: @TContractState) -> u32;

    // Identity binding
    fn set_agent_id(ref self: TContractState, registry: ContractAddress, agent_id: u256);
    fn get_agent_id(self: @TContractState) -> (ContractAddress, u256);

    // Timelocked upgrades
    fn schedule_upgrade(ref self: TContractState, new_class_hash: ClassHash);
    fn execute_upgrade(ref self: TContractState);
    fn cancel_upgrade(ref self: TContractState);
    fn get_pending_upgrade(self: @TContractState) -> (ClassHash, u64);
    fn set_upgrade_delay(ref self: TContractState, delay: u64);
    fn get_upgrade_delay(self: @TContractState) -> u64;

    // Standard account functions (from OpenZeppelin AccountComponent)
    fn get_public_key(self: @TContractState) -> felt252;
    fn set_public_key(ref self: TContractState, new_public_key: felt252, signature: Span<felt252>);
}

Session Keys

Session keys provide temporary, limited access to the account without exposing the owner's private key.

Registering a Session Key

fn register_session_key(ref self: TContractState, key: felt252, policy: SessionPolicy)

Access Control: Owner only

Validation:

  • valid_until must be greater than valid_after
  • valid_until must be in the future

Example:

import { Account, RpcProvider, CallData, constants } from "starknet";

const provider = new RpcProvider({ nodeUrl: process.env.STARKNET_RPC_URL });
const account = new Account(
  { nodeUrl: provider },
  accountAddress,
  privateKey,
  undefined,
  constants.TRANSACTION_VERSION.V3
);

const policy = {
  valid_after: Math.floor(Date.now() / 1000),
  valid_until: Math.floor(Date.now() / 1000) + 86400, // 24 hours
  allowed_contract: "0x0", // Zero = any contract
  spending_token: STRK_ADDRESS,
  spending_limit: { low: "1000000000000000000000", high: "0" }, // 1000 STRK
};

await account.execute({
  contractAddress: agentAccountAddress,
  entrypoint: "register_session_key",
  calldata: CallData.compile({
    key: sessionPublicKey,
    policy: policy,
  }),
});

Spending Limit Enforcement

Session keys track spending per token in a 24-hour rolling window:

  • Tracked selectors: transfer, approve, increase_allowance, increaseAllowance
  • Period resets after 86400 seconds (24 hours)
  • Cumulative spending must not exceed spending_limit
// Session key can spend up to 1000 STRK per 24-hour period
// After period expires, limit resets automatically

Signature Format

The account validates signatures based on length:

  • Owner signature: 2 felts [r, s]
  • Session key signature: 3 felts [session_public_key, r, s]

Emergency Revocation

Instantly revoke all session keys:

fn emergency_revoke_all(ref self: TContractState)

Access Control: Owner only

This function uses compact storage with swap-and-remove to maintain bounded gas costs.

Timelocked Upgrades

Contract upgrades require a time delay to prevent instant malicious upgrades.

Schedule an Upgrade

fn schedule_upgrade(ref self: TContractState, new_class_hash: ClassHash)

Access Control: Owner only

Default delay: 300 seconds (5 minutes)

Execute an Upgrade

fn execute_upgrade(ref self: TContractState)

Access Control: Owner only

Validation: Current time must be >= scheduled_at + upgrade_delay

Cancel an Upgrade

fn cancel_upgrade(ref self: TContractState)

Access Control: Owner only

Example:

// Schedule upgrade
await account.execute({
  contractAddress: agentAccountAddress,
  entrypoint: "schedule_upgrade",
  calldata: CallData.compile({ new_class_hash: newClassHash }),
});

// Wait for delay...
await sleep(300_000); // 5 minutes

// Execute upgrade
await account.execute({
  contractAddress: agentAccountAddress,
  entrypoint: "execute_upgrade",
  calldata: [],
});

Agent Account Factory

The factory deploys agent accounts with integrated ERC-8004 identity.

IAgentAccountFactory

#[starknet::interface]
pub trait IAgentAccountFactory<TContractState> {
    fn deploy_account(
        ref self: TContractState,
        public_key: felt252,
        salt: felt252,
        token_uri: ByteArray
    ) -> (ContractAddress, u256);

    fn get_account_class_hash(self: @TContractState) -> ClassHash;
    fn set_account_class_hash(ref self: TContractState, class_hash: ClassHash);
    fn get_identity_registry(self: @TContractState) -> ContractAddress;
    fn set_identity_registry(ref self: TContractState, registry: ContractAddress);
}

Deploying an Agent Account

const { transaction_hash } = await factoryAccount.execute({
  contractAddress: factoryAddress,
  entrypoint: "deploy_account",
  calldata: CallData.compile({
    public_key: agentPublicKey,
    salt: randomSalt,
    token_uri: "ipfs://QmAgentMetadata",
  }),
});

const receipt = await factoryAccount.waitForTransaction(transaction_hash);
// Parse AccountDeployed event for account_address and agent_id

The factory:

  1. Deploys the agent account contract
  2. Registers an agent in the IdentityRegistry with the provided token URI
  3. Transfers the agent NFT to the deployed account
  4. Binds the identity to the account

ERC-8004 Integration

Bind an on-chain identity to the agent account:

await account.execute({
  contractAddress: agentAccountAddress,
  entrypoint: "set_agent_id",
  calldata: CallData.compile({
    registry: identityRegistryAddress,
    agent_id: agentId,
  }),
});

Query the bound identity:

const contract = new Contract(abi, agentAccountAddress, provider);
const [registry, agentId] = await contract.get_agent_id();

Architecture

+-------------------------------------------------------------+
|                      Agent Account                          |
+-------------------------------------------------------------+
|  +-------------+  +-------------+  +-------------+          |
|  |    Owner    |  |  Session 1  |  |  Session 2  |  ...     |
|  |  (Full Key) |  |  (Limited)  |  |  (Limited)  |          |
|  +-------------+  +-------------+  +-------------+          |
+-------------------------------------------------------------+
|  Validation (__validate__):                                 |
|  - Check signature (owner or valid session)                 |
|  - Verify contract is allowed for session                   |
|  - Check spending limits not exceeded                       |
|  - Verify session not expired                               |
+-------------------------------------------------------------+
|  Execution (__execute__):                                   |
|  - Execute calls if validation passes                       |
|  - Update spending counters for ERC-20 operations           |
|  - Emit events for tracking                                 |
+-------------------------------------------------------------+

Security Model

Defense in Depth

Multiple layers of protection:

  1. Session isolation - Compromise of session key doesn't expose owner key
  2. Spending limits - Even with session key, damage is bounded per 24-hour period
  3. Contract restrictions - Session keys can be limited to specific contracts
  4. Time limits - Sessions automatically expire at valid_until
  5. Emergency revocation - Owner can instantly revoke all session keys
  6. Timelocked upgrades - Prevents instant malicious contract replacement

Threat Model

ThreatMitigation
Session key stolen24-hour spending cap limits losses; time expiry limits window
Malicious contract callallowed_contract restricts which contracts can be called
Long-running compromiseSession expiry; daily limits reset
Draining via approvalsincrease_allowance tracked against spending limit
Malicious upgradeTimelock provides window to cancel

Test Coverage

The Agent Account has comprehensive test coverage:

Test SuiteTestsCoverage
test_agent_accountSession key lifecycle, policies795 lines
test_agent_account_factoryDeployment, identity binding274 lines
test_execute_validateSignature validation, execution507 lines
test_securityEmergency revoke, upgrades, spending1,113 lines
Total110 tests2,689 lines

Reference Implementation

The Agent Account design is inspired by: