# USDC Escrow for AI Agents: How Trustless Freelancing Actually Works

> Source: <https://dev.to/nikhilranka23/usdc-escrow-for-ai-agents-how-trustless-freelancing-actually-works-3gch>
> Published: 2026-09-07 05:32:09+00:00

*Target audience: developers building autonomous AI agents that need to pay for on‑chain services without relying on a central intermediary.* 

Autonomous agents often act as both consumer and provider of on‑chain services (e.g., calling a data oracle, invoking a compute function, or purchasing API access). When the agent must spend funds it does not own, two problems appear:

A trustless escrow solves both by locking funds in a contract that only releases them when a verifiable condition is met. The condition can be as simple as “the caller supplied a valid signature proving the service was executed”, or as complex as a Merkle‑proof of off‑chain work.

Below we walk through a minimal, production‑ready escrow pattern using **USDC on Base** (an EVM‑compatible rollup) and the **x402** payment‑protocol extension, which lets agents attach a payment to an HTTP request in a standard way.  

We keep the contract deliberately small:

`release` only if they present a valid `refund` after a timeout if the service never appears.

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

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

/**
 * @title Simple USDC Escrow for x402‑paid agents
 * @notice Agents deposit USDC; providers release funds by presenting a valid
 *         x402 payment receipt (agent signature over request details).
 */
contract USDCx402Escrow is Ownable {
    IERC20 public immutable usdc;          // USDC on Base (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
    address public immutable agent;        // The AI agent that funds the escrow
    uint256 public constant REFUND_TIMEOUT = 7 days; // Adjust to your SLA

    struct Receipt {
        bytes32 requestId;   // Keccak256 of (method, URL, bodyHash, nonce)
        uint256 amount;      // USDC amount (6 decimals)
        uint256 deadline;    // Unix timestamp after which receipt is stale
        bytes signature;     // Agent's ECDSA signature over keccak256(abi.encodePacked(requestId, amount, deadline))
    }

    mapping(bytes32 => bool) public usedReceipt; // Prevent replay

    event Deposit(address indexed from, uint256 amount);
    event Release(address indexed to, uint256 amount, bytes32 requestId);
    event Refund(address indexed to, uint256 amount);

    constructor(address _usdc, address _agent) {
        require(_usdc != address(0) && _agent != address(0), "zero address");
        usdc = IERC20(_usdc);
        agent = _agent;
    }

    /** Agent deposits USDC into the escrow. */
    function deposit() external payable {
        require(msg.sender == agent, "only agent");
        require(msg.value == 0, "send USDC via ERC20 transfer, not ETH");
        uint256 amount = usdc.balanceOf(address(this));
        usdc.transferFrom(agent, address(this), amount); // Pull previously approved amount
        emit Deposit(agent, amount);
    }

    /** Provider calls this to claim payment. */
    function release(Receipt calldata receipt) external {
        require(!usedReceipt[receipt.requestId], "receipt already used");
        usedReceipt[receipt.requestId] = true;

        // 1️⃣ Verify signature matches the agent
        bytes32 hash = keccak256(
            abi.encodePacked(
                receipt.requestId,
                receipt.amount,
                receipt.deadline
            )
        );
        address signer = ecrecover(hash, uint8(receipt.signature[0]) + 27, receipt.signature[1], receipt.signature[2]);
        require(signer == agent, "invalid agent signature");

        // 2️⃣ Check amount and deadline
        require(receipt.amount > 0, "zero amount");
        require(block.timestamp <= receipt.deadline, "expired receipt");

        // 3️⃣ Transfer USDC
        usdc.transfer(msg.sender, receipt.amount);
        emit Release(msg.sender, receipt.amount, receipt.requestId);
    }

    /** Agent can refund after timeout if no one claimed. */
    function refund() external {
        require(msg.sender == agent, "only agent");
        require(block.timestamp >= REFUND_TIMEOUT, "timeout not reached");
        uint256 bal = usdc.balanceOf(address(this));
        require(bal > 0, "nothing to refund");
        usdc.transfer(agent, bal);
        emit Refund(agent, bal);
    }

    /** Helper for agent to approve escrow to pull USDC. */
    function approveEscrow(uint256 amount) external {
        require(msg.sender == agent, "only agent");
        usdc.approve(address(this), amount);
    }
}
```

`approveEscrow`) and then calls `deposit()`. The escrow now holds the funds.
`requestId` (often `keccak256(abi.encodePacked(method, url, bodyHash, nonce))`), the USDC amount, a deadline, and signs the hash with its private key. The receipt is attached to the HTTP request as an `x402-payment` header (see the x402 spec).
`escrow.release(receipt)`. If the signature matches the agent and the receipt is fresh, the escrow transfers the USDC to the provider.
`refund()` after a configurable timeout, retrieving the deposited USDC.
Below is a minimal example that shows how an autonomous agent would:

``` python
ts
// agent.ts
import { ethers } from "ethers";
import dotenv from "dotenv";
dotenv.config();

const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base
const ESCROW_ADDRESS = process.env.ESCROW_ADDR!; // deployed contract
const AGENT_PRIVATE_KEY = process.env.AGENT_PRIV_KEY!;
const RPC_URL = "https://mainnet.base.org"; // public Base RPC

const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(AGENT_PRIVATE_KEY, provider);
const usdc = new ethers.Contract(USDC_ADDRESS, [
  "function approve(address spender, uint256 amount) returns (bool)",
  "function balanceOf(address) view returns (uint256)",
], wallet);
const escrow = new ethers.Contract(ESCROW_ADDRESS, [
  "function deposit()",
  "function approveEscrow(uint256 amount)",
  "function release(bytes32 requestId, uint256 amount, uint256 deadline, bytes signature)",
], wallet);

/** Helper: keccak256 of a UTF‑8 string */
function keccak256(str: string): string {
  return ethers.keccak256(ethers.toUtf8Bytes(str));
}

/** Build a deterministic requestId */
function makeRequestId(method: string, url: string, body: any, nonce: number): string {
  const bodyHash = ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(body)));
  return ethers.keccak256(
    ethers.concat([
      ethers.toUtf8Bytes(method),
      ethers.toUtf8Bytes(url),
      ethers.toUtf8Bytes(bodyHash),
      ethers.zeroPadValue(BigInt(nonce), 32)
    ])
  );
}

/** Sign the x402 receipt hash */
async function signReceipt(requestId: string, amount: number, deadline: number): Promise<string> {
  const hash = ethers.keccak256(
    ethers.concat([
      ethers.getBytes(requestId),
      ethers.zeroPadValue(BigInt(amount * 1e6), 32), // USDC has 6 decimals
      ethers.zeroPadValue(BigInt(deadline), 32)
    ])
  );
  const signature = await wallet.signMessage(ethers.getBytes(hash));
  return signature; // 65‑byte (r,s,v) signature
}

/** Main flow */
async function run() {
  // 1️⃣ Approve & deposit (run once per funding round)
  const depositAmount = ethers.parseUnits("10", 6); // 10 USDC
  await usdc.approve(ESCROW_ADDRESS, depositAmount);
  const txDeposit = await escrow.deposit();
  await txDeposit.wait();
  console.log("Deposited:", depositAmount / 1e6, "USDC");

  // 2
```


