ERC-8183 - Agentic Commerce

Created 2026-02-25
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This specification defines the Agentic Commerce Protocol: a job with escrowed budget, four states (Open → Funded → Submitted → Terminal), and an evaluator who alone may mark the job completed. The client funds the job; the provider submits work; the evaluator attests completion or rejection once submitted (or the evaluator rejects while Funded before submission, or the client rejects while Open, or the job expires and the client is refunded). Optional attestation reason (e.g. hash) on complete/reject enables audit and composition with reputation (e.g. ERC-8004).

Motivation

Many use cases need only: client locks funds, provider submits work, one attester (evaluator) signals "done" and triggers payment—or client rejects or timeout triggers refund. The Agentic Commerce Protocol specifies that minimal surface so implementations stay small and composable. The evaluator can be the client (e.g. evaluator = client at creation) when there is no third-party attester.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

State Machine

A job has exactly one of six states:

State Meaning
Open Created; budget not yet set or not yet funded. Client may set budget, then fund or reject.
Funded Budget escrowed. Provider may submit work; evaluator may reject. After expiredAt, anyone may trigger refund.
Submitted Provider has submitted work. Only evaluator may complete or reject. After expiredAt, anyone may trigger refund.
Completed Terminal. Escrow released to provider (minus optional platform fee).
Rejected Terminal. Escrow refunded to client.
Expired Terminal. Same as Rejected; escrow refunded to client.

Allowed transitions:

No other transitions are valid.

Roles

Job Data

Each job SHALL have at least:

Payment SHALL use a single ERC-20 token (global for the contract or specified at creation). Implementations MAY support a per-job token; the specification only requires one token per contract.

Optional provider (set later)

Jobs MAY be created without a provider by passing provider = address(0) to createJob. In that case the client SHALL set the provider later via setProvider(jobId, provider) before funding. This supports flows such as bidding or assignment after creation.

Core Functions

Attestation

Fees

Implementations MAY charge a platform fee (basis points) on Completed, paid to a configurable treasury. The specification does not require a fee. If present, fee SHALL be deducted only on completion (not on refund).

Hooks (OPTIONAL)

Implementations MAY support an optional hook contract per job to extend the core protocol without modifying it. The hook address is set at job creation (or address(0) for no hook) and stored on the job. A non‑hooked kernel that ignores the hook field (or always sets it to address(0)) is fully compliant with this specification; the reference AgenticCommerce contract follows this minimal pattern, while AgenticCommerceHooked is an extension that layers the hook callbacks on top of the same lifecycle.

A hook contract SHALL implement the IACPHook interface — just two functions:

interface IACPHook {
    function beforeAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
    function afterAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
}

The selector parameter identifies which core function is being called (e.g. the function selector for fund). The data parameter contains function-specific parameters encoded as bytes (see Data encoding below). The hook uses the selector to route internally:

function beforeAction(uint256 jobId, bytes4 selector, bytes calldata data) external {
    if (selector == FUND_SELECTOR) {
        // custom pre-fund logic using data (optParams)
    } else if (selector == COMPLETE_SELECTOR) {
        // custom pre-complete logic using data (reason, optParams)
    }
}

When a job has a hook set, the core contract SHALL call hook.beforeAction(...) and hook.afterAction(...) around each hookable function:

Core function Hookable
setProvider Yes
setBudget Yes
fund Yes
submit Yes
complete Yes
reject Yes
claimRefund No — permissionless safety mechanism, SHALL NOT be hookable

Data encoding

The data parameter passed to hooks contains the core function's parameters encoded as bytes. The encoding per selector:

Core function data encoding
setProvider abi.encode(address provider, bytes optParams)
setBudget abi.encode(uint256 amount, bytes optParams)
fund optParams (raw bytes)
submit abi.encode(bytes32 deliverable, bytes optParams)
complete abi.encode(bytes32 reason, bytes optParams)
reject abi.encode(bytes32 reason, bytes optParams)

Hook behaviour

Hook security

Convenience base contract (non-normative)

Implementations MAY provide a BaseACPHook that routes the generic beforeAction/afterAction calls to named virtual functions (e.g. _preFund, _postComplete) so hook developers only override what they need. This is NOT part of the standard — only IACPHook is normative.

Example use cases


Example 1 — Fund Transfer Hook (two-phase escrow)

Problem: A client hires an agent to convert/bridge/swap tokens (e.g. USDC → DAI). The client provides capital to the provider, who uses it to produce output tokens. The hook must ensure the provider deposits the output tokens before the job completes, then release them to the designated buyer.

Solution: A FundTransferHook that (a) stores a transfer commitment at setBudget, (b) forwards capital to the provider at fund, (c) pulls output tokens from the provider at submit, and (d) releases them to the buyer at complete.

Step 1  createJob
  Client  createJob(provider, evaluator, expiredAt, desc, hook=FundTransferHook)
  Job created (Open), hook address stored.

Step 2  setBudget
  Client  setBudget(jobId, serviceFee, optParams=abi.encode(buyer, transferAmount))
     hook.beforeAction: decode optParams, store {buyer, transferAmount} as commitment.
     core: job.budget = serviceFee

Step 3  fund
  Client approves: core contract for serviceFee, hook for transferAmount.
  Client  fund(jobId, serviceFee, "")
     hook.beforeAction: verify client approved hook for transferAmount. Revert if not.
     core: pull serviceFee into escrow, set Funded.
     hook.afterAction: pull transferAmount from client, forward to provider (capital).

Step 4  provider uses capital to produce output tokens

Step 5  submit
  Provider approves hook for transferAmount (output tokens).
  Provider  submit(jobId, deliverable, "")
     hook.beforeAction: pull transferAmount from provider into hook (escrow).
     core: set Submitted.

Step 6  complete
  Evaluator  complete(jobId, reason, "")
     core: release serviceFee to provider (minus platform fee).
     hook.afterAction: release transferAmount from hook to buyer.

Recovery:
  - reject: hook.afterAction returns escrowed tokens to provider (if deposited).
  - expiry: claimRefund (not hookable) refunds serviceFee to client.
    Provider calls recoverTokens(jobId) on hook to recover deposited tokens.

Key properties: (1) The provider cannot submit without depositing output tokens. (2) The buyer only receives tokens when the evaluator completes the job. (3) On rejection or expiry, tokens are returned to the provider.


Example 2 — Bidding Hook

Problem: A client wants to hire the cheapest (or best) agent for a job but does not know upfront who to assign. The selection should be determined by an open bidding process, not unilaterally by the client after the fact.

Solution: A BiddingHook that verifies off-chain signed bids. Providers sign bid commitments off-chain; the client collects bids, selects the winner, and submits the winning bid's signature via setProvider. The hook's beforeAction callback recovers the signer and verifies it matches the chosen provider — proving the provider actually committed to that price.

Zero direct calls to the hook. All interactions flow through the core contract → hook callbacks.

Step 1  createJob
  Client  createJob(provider=0, evaluator, expiredAt, desc, hook=BiddingHook)
  Job created (Open), provider = address(0).

Step 2  setBudget (opens bidding via hook callback)
  Client  setBudget(jobId, maxBudget, optParams=abi.encode(biddingDeadline))
     hook.beforeAction: store deadline for this jobId.

Step 3  bidding happens OFF-CHAIN
  Providers sign: keccak256(abi.encode(chainId, hookAddress, jobId, bidAmount))
  Client collects signed bids and selects the winner.
  Core contract is unaware of bids.

Step 4  setProvider + setBudget (hook verifies winning bid signature and enforces budget)
  Client  setProvider(jobId, winnerAddress, optParams=abi.encode(bidAmount, signature))
     hook.beforeAction: verify deadline passed, recover signer from signature,
      validate signer == provider, store committed bidAmount. Revert if invalid.
     core: job.provider = winnerAddress
     hook.afterAction: mark bidding finalised (no further setProvider possible).
  Client  setBudget(jobId, bidAmount, "")
     hook.beforeAction: enforce budget == committedAmount. Revert if mismatch.

Step 5  job continues normally
  Client  fund(jobId, bidAmount, "")
  Provider  submit(jobId, deliverable, "")
  Evaluator  complete(jobId, reason, "")

Key property: The client cannot fabricate a provider commitment. The hook verifies the chosen provider actually signed a bid at the claimed price. The client is incentivised to pick the lowest bidder since they are the one paying.


Events

Implementations SHOULD emit at least:

Rationale

Extensions (OPTIONAL)

The following extensions are OPTIONAL and do not modify the core protocol. Implementations MAY adopt them independently.

Reputation / Attestation Interop (ERC-8004)

Agentic Commerce is intentionally minimal and does not embed a reputation system. For on-chain reputation and trust relationships between agents, implementations are RECOMMENDED to integrate with ERC-8004 (Trustless Agents).

The following patterns are RECOMMENDED:


Meta-Transactions / Facilitator Relay (ERC-2771)

To support gasless execution — where a client, provider, or evaluator signs an intent off-chain and a facilitator submits the transaction on their behalf — implementations SHOULD support ERC-2771 (Secure Protocol for Native Meta Transactions).

How it works:

  1. A participant (client, provider, or evaluator) signs a meta-transaction off-chain (e.g. createJob, fund, submit).
  2. A facilitator submits the signed payload to a trusted forwarder contract.
  3. The forwarder verifies the signature and calls the ACP contract, appending the original signer's address.
  4. The ACP contract uses _msgSender() (from ERC2771Context) instead of msg.sender to identify the caller.

Implementation requirements:

import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";

contract AgenticCommerce is ERC2771Context, ... {
    constructor(address trustedForwarder, ...)
        ERC2771Context(trustedForwarder) { ... }

    // Example: fund() using _msgSender() instead of msg.sender
    function fund(uint256 jobId, uint256 expectedBudget) external {
        Job storage job = jobs[jobId];
        if (_msgSender() != job.client) revert Unauthorized();
        if (job.budget != expectedBudget) revert BudgetMismatch();
        // ...
    }
}

Token approvals: For functions that pull tokens (e.g. fund), the signer SHOULD use ERC-2612 (permit) to approve token spending via signature. The facilitator can then call permit and fund in a single transaction — no on-chain approval tx needed from the signer.

x402 compatibility: This extension enables compatibility with HTTP-native payment protocols such as x402, where an AI agent signs payment intents off-chain and a payment facilitator handles on-chain execution. The agent only needs a private key and tokens — no gas, no RPC management, no chain-specific logic.


Backwards Compatibility

No backward compatibility issues found.

Reference Implementation

TBD

Security Considerations

Copyright

Copyright and related rights waived via CC0.