DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

USDC Escrow for AI Agents: How Trustless Freelancing Actually Works

USDC Escrow for AI Agents: How Trustless Freelancing Actually Works

Target audience: developers building autonomous AI agents that need to earn and spend money without a human intermediary.


Why an escrow matters for AI‑to‑AI payments

When an AI agent offers a service (e.g., “summarize a PDF”, “generate a logo”, “run a small ML inference”), the buyer wants assurance that they only pay if the work is delivered as promised. The seller (the agent) wants assurance that they will receive payment once the work is verified. In a fully autonomous setting there is no trusted third‑party human to mediate disputes, so the trust must be placed in code that runs on a blockchain.

USDC on Base provides a stable, low‑volatility asset that can be moved cheaply (≈ $0.0005 per transaction) and settled in seconds. By locking USDC in a simple escrow contract before work begins, both parties can rely on the blockchain’s guarantee that funds will only move when predefined conditions are met.


Minimal viable escrow design

We’ll use a two‑party escrow that follows this flow:

  1. Buyer deposits USDC into the escrow contract, specifying the seller’s address and a work‑hash (the hash of the expected output).
  2. Seller performs the work off‑chain and submits a proof (e.g., the actual output) together with a cryptographic commitment that matches the work‑hash.
  3. Anyone (or a designated oracle) can call release() if the proof matches the hash; the escrow then transfers the USDC to the seller.
  4. If the seller never submits a valid proof, the buyer can call refund() after a timeout.

The contract does not try to judge the quality of the work; it only verifies that the submitted data matches a pre‑agreed commitment. Quality assessment must happen off‑chain (e.g., via a reputation system, a human reviewer, or another AI verifier). This keeps the on‑chain logic simple, cheap, and auditable.

Solidity (pragma ^0.8.20)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract US CECrow {
    IERC20 public immutable usdc; // USDC on Base (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
    address public buyer;
    address public seller;
    bytes32 public workHash; // keccak256(expectedOutput)
    uint256 public amount;   // amount in USDC (6 decimals)
    uint256 public depositTime;
    uint256 public constant REFUND_TIMEOUT = 1 days; // adjust as needed

    enum State { Created, Funded, Released, Refunded }
    State public state;

    event Deposited(address indexed buyer, address indexed seller, uint256 amount);
    event Released(address indexed seller, uint256 amount);
    event Refunded(address indexed buyer, uint256 amount);

    constructor(address _usdc, address _buyer, address _seller, bytes32 _workHash, uint256 _amount) {
        require(_buyer != address(0) && _seller != address(0), "zero address");
        require(_amount > 0, "zero amount");
        usdc = IERC20(_usdc);
        buyer = _buyer;
        seller = _seller;
        workHash = _workHash;
        amount = _amount;
    }

    /// @notice Buyer calls this to lock funds. Must approve USDC to spend `amount`.
    function deposit() external {
        require(state == State.Created, "not created");
        require(msg.sender == buyer, "only buyer");
        usdc.transferFrom(msg.sender, address(this), amount);
        depositTime = block.timestamp;
        state = State.Funded;
        emit Deposited(buyer, seller, amount);
    }

    /// @notice Anyone can submit a proof. If keccak256(proof) == workHash, funds go to seller.
    function release(bytes calldata proof) external {
        require(state == State.Funded, "not funded");
        require(keccak256(abi.encodePacked(proof)) == workHash, "invalid proof");
        usdc.transfer(seller, amount);
        state = State.Released;
        emit Released(seller, amount);
    }

    /// @notice Buyer can reclaim funds after timeout if seller never proved work.
    function refund() external {
        require(state == State.Funded, "not funded");
        require(block.timestamp >= depositTime + REFUND_TIMEOUT, "timeout not reached");
        require(msg.sender == buyer, "only buyer");
        usdc.transfer(buyer, amount);
        state = State.Refunded;
        emit Refunded(buyer, amount);
    }

    /// @notice Helper for off‑chain scripts to read the escrow balance.
    function escrowBalance() external view returns (uint256) {
        return usdc.balanceOf(address(this));
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points

  • The contract holds USDC via the standard ERC‑20 interface (transferFrom on deposit, transfer on payout).
  • The only on‑chain condition is a hash match (keccak256(proof) == workHash).
  • A simple timeout protects the buyer from forever‑locked funds.
  • No external oracle is needed for the core escrow; any party can submit the proof.

Integrating the escrow from an AI agent

Below is a minimal Node.js example using ethers.js that shows how an autonomous agent could:

  1. Listen for a new escrow creation event (posted by a buyer).
  2. Perform the work off‑chain.
  3. Submit the proof to claim payment.
// escrow-agent.js
require('dotenv').config();
const { ethers } = require('ethers');
const escrowABI = [ /* paste the ABI from the compiled contract */ ];

const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC); // e.g., https://base-mainnet.g.alchemy.com/v2/<key>
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const usdcAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const escrowAddress = '0xEscrowContractAddress'; // set after deployment

const escrow = new ethers.Contract(escrowAddress, escrowABI, wallet);

async function main() {
  // 1️⃣ Wait for a new escrow funded for this agent
  escrow.on('Deposited', async (buyer, seller, amount) => {
    if (seller.toLowerCase() !== wallet.address.toLowerCase()) return; // not for us
    console.log(`💰 Escrow funded: ${ethers.formatUnits(amount, 6)} USDC from ${buyer}`);

    const workHash = await escrow.workHash();
    // In a real agent, you would derive the expected output from the job description
    // For demonstration we assume the buyer already gave us the hash of the expected result.
    // 2️⃣ Do the work off‑chain (e.g., call an LLM, run inference, etc.)
    const result = await performJob(); // <-- replace with your actual logic
    const proof = ethers.toUtf8Bytes(result); // simple proof: the raw output

    // 3️⃣ Submit proof
    const tx = await escrow.release(proof);
    const receipt = await tx.wait();
    console.log(`✅ Released payment: ${receipt.transactionHash}`);
  });
}

async function performJob() {
  // Placeholder: replace with actual AI work (e.g., openai.chat.completions.create)
  return 'This is the summary of the supplied document.';
}

main().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

What this code does

  • Listens for the Deposited event, verifying that the escrow was created for the agent’s address.
  • Retrieves the stored workHash (the commitment the buyer made when funding the escrow).
  • Executes the job off‑chain (performJob).
  • Sends the raw output as the proof. The contract recomputes the hash and releases funds if it matches.

Error handling & retries

  • If the transaction reverts (e.g., proof mismatch), the agent should log the failure and optionally request a new work hash from the buyer (via an off‑chain messaging protocol).
  • Network hiccups are handled by the provider’s automatic retry; for long‑running jobs you may want to persist the job state to a local database so the agent can resume after a restart.

Trade‑offs you’ll encounter

Aspect Benefit Cost / Limitation
On‑chain guarantee Funds cannot be stolen; release is atomic and verifiable by anyone. Requires the buyer to lock funds upfront (capital efficiency loss).
Simple hash‑match Cheap to verify (< 0.0001 ETH gas on Base). Does not assess quality; a malicious seller could submit garbage that still matches the hash if the buyer supplied a wrong hash.
Timeout refund Protects buyer from indefinite lock‑up. The timeout must be long enough for the agent to finish work; too short leads to unnecessary refunds.
USDC on Base Low transaction cost (~$0.0005) and fast finality (~2 seconds). You need Base‑compatible wallets and RPC endpoints; liquidity for USDC must exist on the chain.
Off‑chain work verification Keeps the contract minimal and cheap. You must build a reputation system,

Top comments (0)