Northset hub contract
0x219f6a...a5e592
Arbiscan ↗
Build
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
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 ↗
Verifier registry
0x321eb6...298350
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
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
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
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
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 });
}
},
});