ERC-8001 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 omits privacy, reputation, threshold policies, bonding, and cross-chain semantics. Those are expected as optional modules that reference this specification.
Agents in DeFi/MEV often need to act together without a trusted coordinator. Existing intent standards (e.g., ERC-7521, ERC-7683) define single-initiator flows and do not specify multi-party agreement.
ERC-8001 specifies the smallest on-chain primitive for that gap: an initiator's EIP-712 intent plus per-participant EIP-712/EIP-1271 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.
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
:
Implementations MUST use the canonical enum defined above:
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
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.
Implementations MUST use the following EIP-712 domain:
{name: "ERC-8001", version: "1", chainId, verifyingContract}
Implementations SHOULD expose the domain via ERC-5267.
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
}
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):
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.
Implementations MUST expose the following interface and events.
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);
}
proposeCoordination
:agentId
using ECDSA for EOAs or ERC-1271 for contracts.intent.expiry > block.timestamp
and intent.nonce > agentNonces[agentId]
.agentNonces[agentId] = intent.nonce
.CoordinationProposed
.acceptCoordination
:AcceptanceAttestation
.expiry
for that participant.CoordinationAccepted
with the typed acceptance hash.true
when all required acceptances are present.executeCoordination
:payloadHash
matches the stored hash.CoordinationExecuted
.cancelCoordination
:CoordinationCancelled
.executeCoordination
MUST:block.timestamp < intent.expiry
.block.timestamp < acceptance.expiry
.keccak256(abi.encode(payload))
equals the stored payloadHash
.ERC-8001 defines a single intent nonce per agent: agentNonces[agentId]
. Acceptance nonces are OPTIONAL in ERC-8001. If implemented, they MUST be strictly monotonic per agent.
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
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();
ERC-8001 introduces a new interface. It is compatible with EOA and contract wallets via ECDSA and ERC-1271. It does not modify existing standards.
A permissive reference implementation is provided in 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.
coordinationData
reveals strategy, use a Privacy module with commit-reveal or encryption.Copyright and related rights waived via CC0.