ERC-8492: Settlement Deductions for Task Escrows

Hi everyone,

AI agents now take on paid work on-chain. The client locks the pay in an escrow, and the escrow pays the agent when the work is accepted. As this grows, each payment also has to cover others: a tax that funds public services, a fee to whoever referred the job, or the repayment of a loan the agent took to do the job.

This is a draft ERC for Settlement Deductions. It lets an escrow withhold these amounts from each payout. The escrow does not need to know the rules behind them. None of them can stall the payout. It works for any task escrow, not only those used by agents.

Example: a payout of 100, with a 1% tax, a 2% referral fee and a repayment of 30. The treasury gets 1, the referrer 2, the lender 30. The payee gets 67.

Repository (draft text, reference policy, example escrow, tests): GitHub - unicore-gpu/settlement-deductions: Draft ERC: settlement deductions for task escrows (tax, referral fees, repayments withheld at payout, never stalling settlement) · GitHub

The ERC text is ERC-draft.md in the repository. I will open the PR to ethereum/ERCs next, with this thread as its discussion link.

The slot this fills

Escrows for agent work end in one transfer to the payee. The token-bound task tender draft (ERC-8414) pays the fulfiller on acceptance, verification or timeout. ERC-8183 releases the budget to the provider when the evaluator completes the job.

But at that moment, others often have a claim on the money:

  • A network may fund dispute resolution and monitoring with a small tax on settled work.
  • An agent that referred the job may be owed a share of the pay.
  • A lender may have advanced working capital against a task the payee already won. It wants to be repaid from that task’s payment.

Today these claims depend on the payee’s goodwill, or the escrow hard-codes one fee. Per-job hooks don’t solve it either. The client chooses the hook, so the client can leave it out. And a hook that reverts blocks the job.

So there is no tax that reaches every task, no repayment a lender can rely on, and no guarantee that the payee is paid when the deduction logic breaks.

What it specifies

  • One policy, two calls. The policy has two functions: onTaskCreated(taskId, asset, amountPerSettlement) and deductions(taskId, settlementId, payee, amount) returns (uint256). The escrow names one policy for its whole lifetime. No one can choose, replace or bypass it per task.
  • Payouts cannot be stalled. Every payout asks the policy, including default and timeout payouts. Refunds never ask. The answer counts only if the call succeeds, returns exactly 32 bytes, and takes no more than the payout. Otherwise the payee is paid in full, and the escrow emits DeductionsSkipped. The escrow should copy at most 32 bytes of return data, so that a policy cannot make settlement expensive.
  • No starvation. The policy runs on a fixed gas budget G. The escrow reverts unless gasleft() exceeds G * 64 / 63 plus its own overhead. So no caller can give too little gas to make the policy fail and skip the deductions.
  • Credit, not push. The escrow keeps each deduction d as credit for the policy, and emits SettlementDeducted. The payee receives amount - d. No one else is called during settlement. Anyone can call withdrawCredit(taskId, account), but it pays only account. So a policy must account for credit withdrawn by any caller.
  • Creation may revert, payouts may not. If the policy reverts in onTaskCreated, the task is not created. A policy may refuse a task this way, for example when the tax rate is above the cap. No work has been done yet, so nobody loses anything. Once work is paid for, the payout must go through.
  • Reference policy profile (optional). A public tax schedule, with the rate locked per task at creation. Fees and repayments the payee agreed to are assignments. Only the payee can assign its own payouts. Only the recipient can release an assignment. All deductions together stay under one cap. Anyone can trigger a claim.

Fee-on-transfer and rebasing tokens are out of scope. A policy may refuse them at task creation.

What exists today

  • Reference implementation (Solidity 0.8.24, non-upgradeable). SettlementDeductionPolicy is the reference policy. ExampleEscrow is the smallest escrow that meets the escrow requirements, with partial payouts and refunds.

  • 42 Foundry tests. Every normative requirement has at least one test. They cover:

    • the worked example and cap carry-over;
    • policies that revert, return two words, burn all their gas, or revert with 300 KB of data;
    • a gas sweep showing that no gas limit skips the deductions;
    • strangers withdrawing the policy’s credit;
    • re-entry through the token;
    • fuzzing of the cap, ordering and conservation;
    • invariants (escrow solvency, per-task accounting, token conservation) over random sequences of actions.

    Interface IDs and event topics are pinned as test vectors. I also checked the tests by mutation: removing the starvation check or the per-task accounting makes them fail.

  • Gas. The worst case for the reference policy is about 209,000 gas: a tax plus four fixed-sum assignments to new recipients. The example escrow uses G = 300,000.

  • First integration: ERC-8414. It adds about 60 lines to the reference kernel. With no policy set, the kernel behaves exactly as before. The kernel with the policy built in is 24,490 bytes, under the EIP-170 limit. There are 21 integration tests and 8 conformance tests against the escrow requirements: GitHub - unicore-gpu/task-token-standard at settlement-deductions · GitHub

The code is a draft. It has not been audited.

Questions I would value feedback on

  • Gas budget. Each escrow fixes its own G, but the ERC sets no minimum. Should it set one, so that a policy written for one escrow fits every escrow?
  • Creation vs payout. A policy may block task creation, but never a payout. Is that the right split? Or should a task still be created (untaxed) when the policy fails?
  • Credit vs push. Deductions stay in the escrow until someone withdraws them. So each recipient sends a claim for each task. Does this fit the escrows you work on?
  • Scope. Are there other escrows or payment flows this should cover? Are there deductions that one number per payout cannot express?

On the gas budget, the 64/63 check guarantees G, but G is sized for today’s prices. On the current EIP-8037/8038 numbers a new storage slot costs 110,020 instead of 22,100, and the reference deductions() creates one in _owed for each new recipient, the treasury included, plus _deducted on a task’s first payout. A tax and one new assignee already go past 300,000, so in any transaction under 2^24 gas those payouts settle in full with DeductionsSkipped. Above 2^24 the state gas comes from the reservoir and call{gas: G} no longer caps it, so whoever triggers the payout also picks whether the deductions run. The EF’s glamsterdam-gas-invariants repo lists this as INV-7 and the numbers may still change, but a minimum G in the ERC would be the same kind of fixed number. Creating those slots outside settlement, in assignPayment and onTaskCreated with a nonzero placeholder, leaves deductions() only updating existing slots.

The security note asks a lender to check that the assigning address is the one the escrow will pay. That only holds if the escrow can’t name a different payee after the advance. ExampleEscrow takes the payee as an argument on every pay and payPart, so a creator who pays the agent at a fresh address skips all of the payee’s assignments, the lender’s included. ERC-8414 fixes the fulfiller in submitFulfillment, which is what a lender needs, so that could be an escrow requirement.

claim() works out what the policy received as _deducted minus the credit still in the escrow, which assumes every deductions() call that returned was credited. The reference policy never answers more than amount, so it holds there. A policy built on it that does would count credit it never got, and since one policy holds the balance for every task, those claims get paid out of other tasks’ withdrawals.

Thanks, Carlos. All three points hold. I checked each one against the code and the EIP-8037 text.

1. Gas budget

You are right, and a minimum G would not help. Your suggestion is the better fix: create the slots in onTaskCreated and assignPayment, so deductions() only updates them.

2. Payee identity

Agreed. I will add an escrow requirement: the payee is fixed before the payout, so whoever triggers it cannot choose. ERC-8414 already does this with the fulfiller. ExampleEscrow will fix the payee when the task is created.

3. Claim accounting

The reference policy is safe only because it never answers more than amount. That should be a requirement for any policy that relies on the escrow’s ledger, backed by an explicit check in the reference policy.

The changes will go into the repository and the PR, with an update here once they are in.

Update: the three points above are fixed, together with what further reviews turned up. All changes are in this PR, now merged, and the ERC PR is updated to match.

The three points

  1. deductions() no longer writes any storage slot that was zero. Balances are allocated in onTaskCreated and assignPayment, and claims keep them allocated. The spec now makes this a MUST (6.5).
  2. The payee of each payout is fixed before the payout, and the payout function cannot take it as an argument (5.9). The example escrow fixes it at task creation.
  3. A policy that relies on the escrow’s ledger must never answer more than amount (6.4).

Also new

  • Each taskId is announced once, with a fixed asset.
  • deductionPolicy() and deductionGas() make the policy and G readable on chain.
  • Escrows should credit a payee that refuses a payout (5.11), so a refusal cannot force a refund that skips the deductions. Since EIP-7702, any address can refuse.
  • Clearer definitions of payout and amount, and new security notes.

The reference implementation’s tests now pass under the current gas schedule and under EIP-8037/8038. The reference policy’s worst case is about 107,000 gas today and 173,000 under the new schedule, against G = 300,000.

A few numbers in my first post are out of date. There are now 57 tests. The 8414 integration adds about 70 lines, with 11 conformance tests, and the kernel with the policy built in is 24,567 bytes.

Thanks again for the careful read. More comments are welcome.