Sealed-Bid Award Mechanism for Task Tenders (companion to ERC-8183 / 8195 / 8414)

The gap

The agent task stack now has three ways to post and settle a task. ERC-8183 escrows a single job and lets an evaluator release it. ERC-8195 defines five procurement modes. ERC-8414 makes the task itself an ERC-721 with a vault inside it. All three leave the same question outside the standard: when several agents want one task, who is awarded, and at what price?

  • ERC-8183 runs bidding through an off-chain hook and only receives the result through setProvider (see its Example 2).
  • ERC-8195’s Auction mode relays bids off-chain and hardcodes lowest-bid-wins. The thread already notes there is no bid commitment or reveal structure at the interface level, and that point was never picked up.
  • ERC-8414 says plainly that bidding is a companion extension, not kernel.

So each market writes its own award logic, the same agent bids under three incompatible rules, and no indexer can say what an award meant or whether the price was fair.

What this proposes

A small companion standard that answers only that question. It fixes three things:

  1. A sealed-bid procedure: commit, reveal, award, with a bond that is returned on reveal and slashed on silence.
  2. A stateless mechanism interface, award(bids, reserve, units) → (winner, price), that a tender contract calls once reveals close.
  3. Three normative pricing rules: first-price, second-price (Vickrey), and uniform-price for identical units. Anything else is a plugin that implements the same interface.

It holds no task funds, judges no deliverable, and writes no reputation. It returns an award that an ERC-8183 hook can pass to setProvider and setBudget, that an ERC-8195 Auction-mode contract can read in place of selectLowestBidder, or that an ERC-8414 eligibility profile can require of the fulfiller of record.

Why this shape

The design follows Myerson’s optimal auction results (Mathematics of Operations Research, 1981), read as a procurement problem where the private type is each agent’s cost.

  • Revelation principle. Any feasible auction is equivalent to a direct mechanism: bidders report a type, the mechanism computes an allocation and a payment. So the on-chain interface should be exactly that pair of functions, and commit–reveal is what makes a sealed report credible on a public ledger.
  • Revenue equivalence. The requester’s expected payment is fixed once the allocation rule and the reserve are fixed. A standard therefore only needs to pin down those two, and can leave the pricing rule to a plugin without changing what the requester expects to pay.
  • Second-price with a reserve is optimal in the symmetric regular case, and its payment rule contains no distributional parameters. That is why Vickrey is the recommended default: it stays incentive compatible even when the requester’s beliefs about agent costs are wrong, and autonomous agents from different operators have no shared prior to bid against under first-price.
  • The optimal reserve is strictly tighter than the requester’s outside value, and no-award is a legitimate outcome. Hence reserve is a field of its own rather than a budget the hook happens to see.
  • Discriminating rules and correlated-type mechanisms are deliberately out of scope. The first needs per-bidder beliefs; the second needs the requester to pay losing bidders, which the vault standards forbid. Both remain expressible as plugins.

Interfaces

interface IAwardMechanism {
    struct Bid   { address bidder; uint256 amount; }   // amount = price asked
    struct Award { address winner; uint256 price;  }

    function mechanismId() external pure returns (bytes4);   // bytes4(keccak256("award.vickrey")) etc.

    /// MUST be pure. Bids arrive in commit order. Empty return = no award.
    /// No winner may have bid above reserve; no price may exceed reserve;
    /// lowering one bid must never remove that bidder from the award; ties go to the earlier commit.
    function award(Bid[] calldata bids, uint256 reserve, uint256 units)
        external pure returns (Award[] memory);
}

interface ISealedBidTender {
    enum Phase { None, Commit, Reveal, Awarded, Void }

    struct TenderTerms {
        bytes32 taskRef;         // the task in whichever escrow standard opened this tender
        address mechanism;       // IAwardMechanism
        uint256 reserve;         // > 0
        uint256 units;           // >= 1
        uint64  commitDeadline;
        uint64  revealDeadline;  // > commitDeadline
        uint256 bond;            // escrowed per commit, returned on reveal, slashed otherwise
        address bondAsset;       // address(0) = native
    }

    event TenderOpened(bytes32 indexed tenderId, bytes32 indexed taskRef, address indexed requester,
                       address mechanism, uint256 reserve, uint256 units, uint64 commitDeadline, uint64 revealDeadline);
    event BidCommitted(bytes32 indexed tenderId, address indexed bidder, bytes32 commitment);
    event BidRevealed(bytes32 indexed tenderId, address indexed bidder, uint256 amount);
    event BidSlashed(bytes32 indexed tenderId, address indexed bidder, uint256 bond);
    event TenderAwarded(bytes32 indexed tenderId, address indexed winner, uint256 price, bytes4 mechanismId);
    event TenderVoid(bytes32 indexed tenderId);

    function openTender(TenderTerms calldata terms) external returns (bytes32 tenderId);
    // commitment = keccak256(abi.encode(tenderId, msg.sender, amount, salt))
    function commitBid(bytes32 tenderId, bytes32 commitment) external payable;
    function revealBid(bytes32 tenderId, uint256 amount, bytes32 salt) external;
    function finalize(bytes32 tenderId) external returns (IAwardMechanism.Award[] memory);

    function termsOf(bytes32 tenderId) external view returns (TenderTerms memory);
    function requesterOf(bytes32 tenderId) external view returns (address);
    function phaseOf(bytes32 tenderId) external view returns (Phase);
    function awardOf(bytes32 tenderId) external view returns (IAwardMechanism.Award[] memory);
}

Normative rules, with revealed amounts sorted b(1) ≤ b(2) ≤ … and reserve r:

id winners price award iff
award.first-price lowest units bids own bid bid ≤ r
award.vickrey (units = 1) lowest bid min(b(2), r), or r if alone b(1) ≤ r
award.uniform-price lowest units bids min(b(units+1), r) bid ≤ r

Uniform-price is scoped to one unit per bidder; multi-unit demand should use a VCG plugin.

What I would like the forum’s view on

  1. Reserve as a first-class field. Should the escrow standards expose a reserve separately from budget or maxPrice, given that the optimal reserve is tighter and non-award must be allowed?
  2. Bidder heterogeneity. ERC-8004 reputation makes agents asymmetric, and asymmetric optimal auctions discriminate. Should scoring rules be a mechanism plugin, or stay out of the standard entirely?
  3. Bonds. An unrevealed commitment has to cost something or commit–reveal is free to grief. Kernel field, as drafted, or an admission hook?
  4. Composition. Is a stateless IAwardMechanism enough for ERC-8183’s hook, ERC-8195’s selectWorker, and ERC-8414’s eligibility profile, or does each need an adapter? In particular, for ERC-8183: is a hook permitted to call setProvider and setBudget back on the job, or must the client sign those?

The full draft text follows the ERC template and will go up as a PR to ethereum/ERCs once this thread has had a first round. Reference implementation and test vectors to follow; the vectors assert the monotonicity condition and, for Vickrey, that truthful bidding is dominant. Spec text is CC0.

I think the composition question is the one thing to close before this goes to a PR. A valid auction result should not itself acquire authority to mutate whichever task standard it points at. The companion should produce an award; the target standard’s existing authority model should decide how that award becomes effective.

So I would separate:

auction validity → Award(winner, price)

from:

Award → authorized target transition

and make the second step adapter-specific.

For 8183, that means the client still calls setProvider / setBudget; the hook verifies that those values equal the committed award. The hook does not become the client.

For 8195, the adapter projects the award into its Auction transition.

For 8414, the award supplies eligibility only. winner can be consumed by the committed eligibility policy; price does not rewrite rewardPerCompletion, which is already an immutable tender term. The existing versioned TaskRoot and submission taskVersion already determine which eligibility policy applies, so I would not add a second version-binding mechanism here.

The one common binding the companion does need is the target itself. bytes32 taskRef is too underspecified across three standards with different identifier domains. I would define one canonical target commitment, e.g.

targetRef = keccak256(abi.encode(chainId, targetContract, targetId))

with a specified normalization of targetId.

Then the boundary is closed:

valid award
+
canonical target
+
target-authorized adapter
→
effective award

Bonds can stay exactly where they are: they secure the companion’s commit/reveal procedure, not the task kernel. If slashed value is routed anywhere else, that destination should simply be an explicit tender term rather than an incidental property of an adapter.

That leaves the companion doing one job: produce a replay-safe, target-bound award. Each task standard remains authoritative over what that award is allowed to change.