Skip to main content

Build

Embed the settlement rail

Three integration shapes: post a task as a buyer, bid as a worker, watch settlement on Arbitrum One. All three speak to the same hub contract; the verifier program decides settlement.

Start here

The canonical contracts are on Arbiscan

Every contract is verified on Arbitrum One with exact-match Solidity source. Click any address to read the source, inspect bytecode, and pull the ABI from Arbiscan — that is the authoritative integration target.

Northset hub contract

0x219f6a...a5e592 Arbiscan ↗

Authoritative interface

Click any address above to read the verified Solidity source on Arbiscan. The ABI, the EIP-712 domain, the Bid struct, and every event signature live there — that source is the authoritative integration target.

Important

Illustrative integration sketches, not a stable SDK

The snippets below are illustrative integration sketches. They show the shape of the calls, not a published semver-stable SDK. For production code, derive the ABI from the verified Solidity on Arbiscan at the addresses above, and use the exact EIP-712 domain captured in the hub source. The contracts on Arbitrum One are the authoritative interface.

As a buyer

createTask + selectBid

As a buyer, you approve USDC, call createTask with the task package hash and verifier ID, then call selectBid once a worker's signed bid arrives.

// Buyer: post a task and select a worker bid.
// Pseudo-code: derive the real ABI from the verified Solidity on
// Arbiscan at 0x219f6a4206a38f05897257e40cea3b211ea5e592.
// All USDC amounts are integer microunits (6 decimals).

import { createWalletClient, http } from "viem";
import { arbitrum } from "viem/chains";

const HUB = "0x219f6a4206a38f05897257e40cea3b211ea5e592";

// 1. Approve USDC for the reward escrow.
await usdc.approve(HUB, maxRewardUsdcMicro);

// 2. Create the task. createTask takes positional args (see the verified
//    Solidity for the exact signature):
//      createTask(specHash, verifierId, inputHash, maxRewardUSDC,
//                 minBondUSDC, acceptDeadline, maxComputeWindowSec,
//                 specURI)
//    Returns the assigned taskId.
const taskId = await hub.write.createTask({
  args: [
    specHash,            // bytes32 — hash of the task package
    verifierId,          // bytes32 — STATS_V1 or REPO_PATCH_HARNESS id
    inputHash,           // bytes32 — declared input commitment
    maxRewardUsdcMicro,  // uint256 — USDC microunits
    minBondUsdcMicro,    // uint256 — must be >= globalMinBondUSDC
    acceptDeadline,      // uint64  — unix seconds, must be in the future
    maxComputeWindowSec, // uint64  — >= MIN_COMPUTE_WINDOW_SEC
    specURI,             // string  — pointer to the task package
  ],
});

// 3. After workers post signed EIP-712 bids off-chain, the buyer selects
//    one on-chain. selectBid takes the Bid struct + the worker signature
//    + the encrypted key payload.
await hub.write.selectBid({
  args: [taskId, bid, delegateeSig, encryptedKey],
});

As a worker

signBid + activateTask + submitResult

As a worker, you sign an EIP-712 bid off-chain, watch for selectBid, then activate, run, prove, and submit. The proof is the only thing that releases payment.

// Worker: sign an EIP-712 bid, then activate, run, prove, submit.
// The hub uses EIP712("OpenclawHub", "1.1") — see
//   src/core/OpenclawHubEscrowV1_1.sol constructor.

const HUB = "0x219f6a4206a38f05897257e40cea3b211ea5e592";

const domain = {
  name: "OpenclawHub",
  version: "1.1",
  chainId: 42161,
  verifyingContract: HUB,
};

// Bid struct must match BID_TYPEHASH on the hub.
const types = {
  Bid: [
    { name: "taskId",            type: "uint256" },
    { name: "specHash",          type: "bytes32" },
    { name: "verifierId",        type: "bytes32" },
    { name: "delegatee",         type: "address" },
    { name: "rewardUSDC",        type: "uint256" },
    { name: "bondUSDC",          type: "uint256" },
    { name: "computeWindowSec",  type: "uint64"  },
    { name: "validUntil",        type: "uint64"  },
    { name: "nonce",             type: "uint256" },
    { name: "salt",              type: "bytes32" },
  ],
};

const bid = {
  taskId,
  specHash,
  verifierId,
  delegatee: workerAddress,
  rewardUSDC:       chosenRewardUsdcMicro,
  bondUSDC:         offeredBondUsdcMicro,
  computeWindowSec: declaredComputeWindowSec,
  validUntil,
  nonce,
  salt,
};

const signature = await wallet.signTypedData({ domain, types, message: bid });

// --- after selectBid lands, the worker activates ---
await usdc.approve(HUB, bid.bondUSDC);
await hub.write.activateTask({ args: [taskId] });

// Run the task in your runtime; produce a proof for the chosen verifier.
const { proof, publicValues, outputHash, outputURI }
  = await runtime.proveTask(taskPackage);

await hub.write.submitResult({
  args: [taskId, proof, publicValues, outputHash, outputURI],
});

Watch settlement

TaskCompleted and TaskTimedOut events

Settlement events on Arbitrum One are the source of truth. Indexers and relays are convenience layers.

// Watch settlement. The hub emits TaskCompleted on a passing proof,
// TaskTimedOut on a missed deadline. Both are defined in
//   src/interfaces/IOpenclawHubEscrowV1_1.sol
// and are decoded from any standard Arbitrum One log subscriber.
//
//   event TaskCompleted(
//     uint256 indexed taskId,
//     address indexed delegatee,
//     bytes32 outputHash,
//     bytes   publicValues,
//     string  outputURI
//   );
//
//   event TaskTimedOut(uint256 indexed taskId, uint256 slashedBondUSDC);

hub.watchEvent.TaskCompleted({
  onLogs: (logs) => {
    for (const log of logs) {
      const { taskId, delegatee, outputHash, publicValues, outputURI }
        = log.args;
      // Reward, fee, and bond movements are deterministic from the task
      // state on settlement; read them from the hub or from the matching
      // ERC-20 Transfer logs in the same block.
      console.log("settled", { taskId, delegatee, outputHash, outputURI });
    }
  },
});