---
eip: 8001
title: Agent Coordination Framework
description: Minimal, single-chain, multi-party agent coordination using EIP-712 attestations
author: Kwame Bryan (@KBryan)
discussions-to: https://ethereum-magicians.org/t/erc-8001-secure-intents-a-cryptographic-framework-for-autonomous-agent-coordination-draft-erc-8001/24989
status: Final
type: Standards Track
category: ERC
created: 2025-08-02
requires: 712, 1271, 2098, 5267
---

## Abstract

[ERC-8001](./eip-8001.md) defines a minimal, single-chain primitive for **multi-party agent coordination**. An initiator posts an intent and each participant provides a verifiable acceptance attestation. Once the required set of acceptances is present and fresh, the intent is executable. The standard specifies typed data, lifecycle, mandatory events, and verification rules compatible with [EIP-712], [ERC-1271], [EIP-2098], and [EIP-5267].

[ERC-8001](./eip-8001.md) omits privacy, reputation, threshold policies, bonding, and cross-chain semantics. Those are expected as optional modules that reference this specification.

## Motivation

Agents in DeFi/MEV/Web3 Gaming and Agentic Commerce often need to act together without a trusted coordinator. Existing intent standards (e.g., [ERC-7521](./eip-7521.md), [ERC-7683](./eip-7683.md)) define single-initiator flows and do not specify multi-party agreement.

[ERC-8001](./eip-8001.md) specifies the smallest on-chain primitive for that gap: an initiator's [EIP-712](./eip-712.md) intent plus per-participant [EIP-712](./eip-712.md)/[EIP-1271](./eip-1271.md) acceptances. The intent becomes executable only when the required set of acceptances is present and unexpired. Canonical (sorted-unique) participant lists and standard typed data provide replay safety and wallet compatibility. Privacy, thresholds, bonding, and cross-chain are left to modules.

## Specification

The keywords “MUST”, “SHOULD”, and “MAY” are to be interpreted as described in RFC 2119 and RFC 8174.

Implementations MUST expose the following canonical status codes for `getCoordinationStatus`:

### Status Codes

Implementations MUST use the canonical enum defined below:

```solidity
enum Status { None, Proposed, Ready, Executed, Cancelled, Expired }
```
- `None` = default zero state (intent not found)

- `Proposed` = intent proposed, not all acceptances yet

- `Ready` = all participants have accepted, intent executable

- `Executed` = intent successfully executed

- `Cancelled` = intent explicitly cancelled

- `Expired` = intent expired before execution

### Overview

This ERC specifies:
- A canonicalised EIP-712 domain for agent coordination,
- Typed data structures (`AgentIntent`, `CoordinationPayload`, `AcceptanceAttestation`),
- Deterministic hashing rules,
- A standard interface (`IAgentCoordination`),
- Lifecycle semantics (propose → accept → execute/cancel),
- Error surface and status codes.

### EIP-712 Domain

Implementations MUST use the following EIP-712 domain:

```
{name: "ERC-8001", version: "1", chainId, verifyingContract}
```

Implementations SHOULD expose the domain via [ERC-5267](./eip-5267.md).

### Primary Types

```solidity
struct AgentIntent {
    bytes32 payloadHash;           // keccak256(CoordinationPayload)
    uint64  expiry;                // unix seconds; MUST be > block.timestamp at propose
    uint64  nonce;                 // per-agent nonce; MUST be > agentNonces[agentId]
    address agentId;               // initiator and signer of the intent
    bytes32 coordinationType;      // domain-specific type id, e.g. keccak256("MEV_SANDWICH_COORD_V1")
    uint256 coordinationValue;     // informational in Core; modules MAY bind value
    address[] participants;        // unique, ascending; MUST include agentId
}

struct CoordinationPayload {
    bytes32 version;               // payload format id
    bytes32 coordinationType;      // MUST equal AgentIntent.coordinationType
    bytes   coordinationData;      // opaque to Core
    bytes32 conditionsHash;        // domain-specific
    uint256 timestamp;             // creation time (informational)
    bytes   metadata;              // optional
}

struct AcceptanceAttestation {
    bytes32 intentHash;            // getIntentHash(intent)
    address participant;           // signer
    uint64  nonce;                 // optional in Core; see Nonces
    uint64  expiry;                // acceptance validity; MUST be > now at accept and execute
    bytes32 conditionsHash;        // participant constraints
    bytes   signature;             // ECDSA (65 or 64 bytes) or ERC-1271
}
```

### Typed Data Hashes

```solidity

bytes32 constant AGENT_INTENT_TYPEHASH = keccak256(
  "AgentIntent(bytes32 payloadHash,uint64 expiry,uint64 nonce,address agentId,bytes32 coordinationType,uint256 coordinationValue,address[] participants)"
);

bytes32 constant ACCEPTANCE_TYPEHASH = keccak256(
// Field names MUST exactly match the Solidity struct.
  "AcceptanceAttestation(bytes32 intentHash,address participant,uint64 nonce,uint64 expiry,bytes32 conditionsHash)"
);

// participants MUST be unique and strictly ascending by uint160(address).
  function _participantsHash(address[] memory ps) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked(ps));
  }

  function _agentIntentStructHash(AgentIntent calldata i) internal pure returns (bytes32) {
    return keccak256(abi.encode(
      AGENT_INTENT_TYPEHASH,
      i.payloadHash,
      i.expiry,
      i.nonce,
      i.agentId,
      i.coordinationType,
      i.coordinationValue,
      _participantsHash(i.participants)
    ));
  }

// Full EIP-712 digest for the initiator’s signature.
  function _agentIntentDigest(bytes32 domainSeparator, AgentIntent calldata i) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked("\x19\x01", domainSeparator, _agentIntentStructHash(i)));
  }

  function _acceptanceStructHash(AcceptanceAttestation calldata a) internal pure returns (bytes32) {
    // a.intentHash MUST be the AgentIntent struct hash, not the digest.
    return keccak256(abi.encode(
      ACCEPTANCE_TYPEHASH,
      a.intentHash,
      a.participant,
      a.nonce,
      a.expiry,
      a.conditionsHash
    ));
  }

  function _acceptanceDigest(bytes32 domainSeparator, AcceptanceAttestation calldata a) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked("\x19\x01", domainSeparator, _acceptanceStructHash(a)));
  }

```
Computation (normative):

```solidity
participantsHash = keccak256(abi.encodePacked(participants)); // sorted unique
intentStructHash = keccak256(abi.encode(
  AGENT_INTENT_TYPEHASH,
  payloadHash, expiry, nonce, agentId, coordinationType, coordinationValue, participantsHash
));
intentDigest = keccak256("\x19\x01" || domainSeparator || intentStructHash);

```

Clarifications (normative):
- `getIntentHash(intent)` MUST return `intentStructHash` (struct hash), not the full digest.
- `AcceptanceAttestation.intentHash` MUST be that struct hash.
- Each acceptance is signed over its own EIP-712 digest that includes this field.
- `participants` MUST be strictly ascending by `uint160(address)` and deduplicated.


### Interface

Implementations **MUST** expose the following interface and events.

```solidity
interface IAgentCoordination {
    event CoordinationProposed(bytes32 indexed intentHash, address indexed proposer, bytes32 coordinationType, uint256 participantCount, uint256 coordinationValue);
    event CoordinationAccepted(bytes32 indexed intentHash, address indexed participant, bytes32 acceptanceHash, uint256 acceptedCount, uint256 requiredCount);
    event CoordinationExecuted(bytes32 indexed intentHash, address indexed executor, bool success, uint256 gasUsed, bytes result);
    event CoordinationCancelled(bytes32 indexed intentHash, address indexed canceller, string reason, uint8 finalStatus);

    function proposeCoordination(AgentIntent calldata intent, bytes calldata signature, CoordinationPayload calldata payload) external returns (bytes32 intentHash);
    function acceptCoordination(bytes32 intentHash, AcceptanceAttestation calldata attestation) external returns (bool allAccepted);
    function executeCoordination(bytes32 intentHash, CoordinationPayload calldata payload, bytes calldata executionData) external returns (bool success, bytes memory result);
    function cancelCoordination(bytes32 intentHash, string calldata reason) external;

    function getCoordinationStatus(bytes32 intentHash) external view returns (Status status, address proposer, address[] memory participants, address[] memory acceptedBy, uint256 expiry);
    function getRequiredAcceptances(bytes32 intentHash) external view returns (uint256);
    function getAgentNonce(address agent) external view returns (uint64);
}
```

### Semantics

The functions defined in this specification MUST exhibit the following externally observable behaviours.  
This standard does NOT prescribe storage layout, execution model, or internal mechanisms.

#### `proposeCoordination`

`proposeCoordination` MUST revert if:

- the signature does not validate the supplied `AgentIntent` under the ERC-8001 EIP-712 domain;
- `intent.expiry <= block.timestamp`;
- `intent.nonce` is not strictly greater than `getAgentNonce(intent.agentId)`;
- `participants` is not strictly ascending and unique;
- `intent.agentId` is not included in the participants list.

If valid:

- `CoordinationProposed` MUST be emitted;
- `getCoordinationStatus` MUST report `Proposed`;
- `getAgentNonce(intent.agentId)` MUST equal the supplied nonce;
- `getRequiredAcceptances(intentHash)` MUST equal the number of participants.

#### `acceptCoordination`

`acceptCoordination` MUST revert if:

- the intent does not exist or has expired;
- the caller is not listed as a participant;
- the participant has already accepted;
- the attestation signature does not validate under the ERC-8001 domain;
- `attestation.expiry <= block.timestamp`.

If valid:

- `CoordinationAccepted` MUST be emitted;
- the participant MUST appear in the `acceptedBy` list returned by `getCoordinationStatus`;
- if all participants have accepted:
  - the function MUST return `true`;
  - status MUST be `Ready`.

Otherwise the function MUST return `false`.

#### `executeCoordination`

`executeCoordination` MUST revert if:

- the intent is not in `Ready` state;
- `intent.expiry <= block.timestamp`;
- any acceptance has expired;
- the supplied payload does not hash to `payloadHash`.

If valid:

- the implementation MUST attempt execution of the behaviour represented by `executionData`;
- the function MUST return `(success, result)`;
- `CoordinationExecuted` MUST be emitted;
- `getCoordinationStatus` MUST report `Executed`.

#### `cancelCoordination`

- If the intent has not expired, only the proposer MUST be permitted to cancel.
- After expiry, any caller MUST be permitted to cancel.

On success:

- `CoordinationCancelled` MUST be emitted;
- status MUST be `Cancelled`.

#### `getCoordinationStatus`

`getCoordinationStatus(intentHash)` MUST return:

- `None` if the intent does not exist;
- `Proposed` if not all participants have accepted and the intent has not expired;
- `Ready` if all participants have accepted and expiries have not elapsed;
- `Executed` if execution has occurred;
- `Cancelled` if cancellation has occurred;
- `Expired` if the intent has expired and was not executed or cancelled.

#### Nonces

- `getAgentNonce(agent)` MUST increase for every valid new intent.
- `proposeCoordination` MUST reject nonces not strictly greater than the stored nonce.
- Acceptance-level nonces MAY be implemented; if so, they MUST be strictly monotonic per participant.

### Errors

Implementations SHOULD revert with descriptive custom errors (or equivalent revert strings) for the following baseline conditions, and MAY define additional errors for domain-specific modules (e.g. slashing, reputation, or privacy conditions):
- Expired intent
- Bad signature
- Non-participant
- Duplicate acceptance
- Acceptance expired at execute
- Payload hash mismatch

```solidity
error ERC8001_NotProposer();
error ERC8001_ExpiredIntent();
error ERC8001_ExpiredAcceptance(address participant);
error ERC8001_BadSignature();
error ERC8001_NotParticipant();
error ERC8001_DuplicateAcceptance();
error ERC8001_ParticipantsNotCanonical();
error ERC8001_NonceTooLow();
error ERC8001_PayloadHashMismatch();
error ERC8001_NotReady();
```

## Rationale

- Sorted participant lists remove hash malleability and allow off-chain deduplication.
- Separation of intent and acceptance allows off-chain collation and a single on-chain check.
- Keeping [ERC-8001](./eip-8001.md) single-chain avoids coupling to bridge semantics and keeps the primitive audit-friendly.
- Wallet friendliness: EIP-712 arrays let signers see actual participant addresses.

## Backwards Compatibility

[ERC-8001](./eip-8001.md) introduces a new interface. It is compatible with EOA and contract wallets via ECDSA and ERC-1271. It does not modify existing standards.

## Reference Implementation

A permissive reference implementation is provided in [`contracts/AgentCoordination.sol`](../assets/eip-8001/contracts/AgentCoordination.sol). It uses a minimal ECDSA helper and supports ERC-1271 signers. It enforces participant canonicalisation, intent nonces, acceptance freshness, and all-participants policy.

## Security Considerations

- **Replay**: EIP-712 domain binding and monotonic nonces prevent cross-contract replay.
- **Malleability**: Low-s enforcement and 64/65-byte signature support are required.
- **Equivocation**: A participant can sign conflicting intents. Mitigate with module-level slashing or reputation.
- **Liveness**: Enforce TTL on both intent and acceptances. Executors should ensure enough time remains.
- **MEV**: If `coordinationData` reveals strategy, use a Privacy module with commit-reveal or encryption.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).

[EIP-712]: ./eip-712.md
[ERC-1271]: ./eip-1271.md
[EIP-2098]: ./eip-2098.md
[EIP-5267]: ./eip-5267.md
