Execute API

Quickstart

Create an executable address, check it, and watch it execute. The Execute API lives on api.execute.cash.

  1. Create an API key

    Get an API key from the dashboard. It’s secret: keep it on your server, and send it as a bearer token.

    Get an API key

    Creating addresses from a web page instead? Use a client ID.

  2. Create an executable address

    Call POST /v1/address/create with your intent. Every field is required, and nothing else is accepted.

    FieldTypeWhat it is
    tokenaddressThe ERC-20 your user sends. One of the chain’s tokens.
    amountstringExactly what the calls spend, in base units: "25000000" is 25 USDC.
    callsarrayWhat the address does once it holds the amount: contracts, functions and arguments, run in order.
    expirationTimestampintegerUnix seconds, 1 to 5 minutes from now. After it, everything goes to recovery.
    recoveryaddressReceives anything that can’t be spent: excess, late payments, everything after expiry. Usually your user.
    saltstring32 random bytes, as lowercase hex. Makes the address unique.
    chainIdintegerThe chain the address lives on, e.g. 8453 for Base.

    This intent pays a merchant 25 USDC on Base, and refunds your user anything that can’t be spent:

    Request
    MERCHANT=0x…   # who gets paid
    CUSTOMER=0x…   # who gets back anything that can't be spent
    
    curl https://api.execute.cash/v1/address/create \
      -H "Authorization: Bearer $EXECUTE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "amount": "25000000",
        "calls": [
          {
            "target": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
            "function": "transfer(address to, uint256 amount)",
            "args": ["'$MERCHANT'", "25000000"]
          }
        ],
        "expirationTimestamp": '$(( $(date +%s) + 300 ))',
        "recovery": "'$CUSTOMER'",
        "salt": "0x'$(openssl rand -hex 32)'",
        "chainId": 8453
      }'
    Response
    {
      "address": "0x7dFDdbf4b14DB25b020fBF8679a2e688613a2f35",
      "factory": "0x6D85B9706D8f076cB8A9fEA37d70Fcb8D22C2952",
      "intent": {
        "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "amount": "25000000",
        "calls": [
          {
            "target": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
            "function": "transfer(address to, uint256 amount)",
            "args": [
              "0x9F0E8F6D3B1a2c4E5d7F8091a2b3C4D5E6F70819",
              "25000000"
            ]
          }
        ],
        "expirationTimestamp": 1790671800,
        "recovery": "0xc80d2a01247aD9e11D1Ab45B5AcE404B416aF086",
        "salt": "0x2f1c5b0a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a291807f6e5d4c3b2a19080",
        "chainId": 8453
      },
      "terms": "0x000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913…"
    }

    Every field, type and error is in Create an address.

  3. Verify an executable address

    POST /v1/address/create returns the executable address for your intent. There are three ways to check that the address belongs to your intent and nothing else:

    1. Open its page at execute.cash/address/<address>. The page fetches the address’s intent and recomputes the address from it, in your browser.
    2. Look it up with GET /v1/address/<address>, which returns the intent Execute holds for the address, and compare it with yours.
    3. Recompute it yourself. The address is a function of the intent, and PaymentFactory computes it onchain. This asks the factory:
    verify.ts
    import { createPublicClient, encodeFunctionData, http, isAddressEqual, parseAbi, parseAbiItem, type AbiFunction, type AbiParameter, type Address, type Hex } from "viem";
    import { arbitrum, base, monad } from "viem/chains";
    
    type Intent = {
      token: Address;
      amount: string;
      calls: { target: Address; function: string; args: unknown[] }[];
      expirationTimestamp: number;
      recovery: Address;
      salt: Hex;
      chainId: number;
    };
    
    const FACTORY = "0x6D85B9706D8f076cB8A9fEA37d70Fcb8D22C2952";
    const CHAINS = { [base.id]: base, [arbitrum.id]: arbitrum, [monad.id]: monad };
    const factoryAbi = parseAbi([
      "function paymentAddress(address token, uint256 amount, (address target, bytes data)[] calls, uint64 expirationTimestamp, address recovery, bytes32 salt, uint256 chainId) view returns (address)",
    ]);
    
    /** An argument as viem takes it: the API sends integers as base-10 strings, viem wants bigints. */
    function toArg(param: AbiParameter, value: unknown): unknown {
      const array = /^(.*)\[\d*\]$/.exec(param.type);
      if (array) return (value as unknown[]).map((v) => toArg({ ...param, type: array[1] }, v));
      if (param.type === "tuple") {
        const { components } = param as { components: readonly AbiParameter[] };
        return (value as unknown[]).map((v, i) => toArg(components[i], v));
      }
      return /^u?int\d*$/.test(param.type) ? BigInt(value as string) : value;
    }
    
    /** Whether `address` is the executable address of `intent`, as PaymentFactory itself computes it. */
    export async function verify(address: Address, intent: Intent): Promise<boolean> {
      const chain = CHAINS[intent.chainId as keyof typeof CHAINS];
      const client = createPublicClient({ chain, transport: http() });
      const calls = intent.calls.map((call) => {
        const fn = parseAbiItem(`function ${call.function}`) as AbiFunction;
        const args = fn.inputs.map((param, i) => toArg(param, call.args[i]));
        return { target: call.target, data: encodeFunctionData({ abi: [fn], args }) };
      });
      const derived = await client.readContract({
        address: FACTORY,
        abi: factoryAbi,
        functionName: "paymentAddress",
        args: [intent.token, BigInt(intent.amount), calls, BigInt(intent.expirationTimestamp), intent.recovery, intent.salt, BigInt(intent.chainId)],
      });
      return isAddressEqual(derived, address);
    }

    Or have your coding agent do it, and explain what the address will do:

  4. Pay it, and track its execution

    The address’s page, execute.cash/address/<address>, is a live monitor. Send it the tokens and watch the payment get detected and the intent executed and confirmed onchain.

    From your server, poll GET /v1/address/<address>/status, or receive webhooks as it changes. Its state is one of:

    StateWhat it means
    awaiting_fundsWaiting for the amount. balance is what has arrived so far.
    fundedIt holds the amount, and Execute is executing it.
    failingFunded, but a call reverts (a vault at capacity, say). Execute retries until expiry; message and revert say why.
    settledEvery call ran. transactionHash and blockNumber point to it.
    refundingPast expiry with a balance, which is on its way to recovery.
    refundedThe balance went to recovery. amount says how much.
    expiredExpired with nothing to return.
  5. Try it: Execute in an app

    This is what integrating Execute gives your users. Enter your address, pay the executable address from any wallet, and watch it execute.

    Live demo · Base

    Pay yourself 0.000001 USDC through an executable address.

    It’s a real address on Base whose one call sends the USDC back to the address you give here.