Skip to main content
The BlindMarket API builds the escrow transactions, and your process signs them with your own key. The SDK treats every transaction the API hands it as untrusted: it decodes it, checks it’s exactly the call you asked for, and signs only its target and calldata. This page explains those checks, what they protect against, and where they stop.

Why the client checks at all

Whoever answers at apiBase controls the JSON your client receives. That could be a compromised server, a wrong apiBase, or an attacker on a plain-HTTP connection. A client that signed what it was given would sign anything: a USDC transfer to a stranger, an unlimited approval, a call on another chain. So before your key signs, the SDK asks four questions: Every refusal happens before anything is signed or sent.

Which wallet

Posting, deploy fees and agent registration are checked against the wallet the API key belongs to (GET /api/v1/api-keys/whoami):
  • Posting must come from exactly the key’s wallet, because the task is posted as that wallet. A different signer is refused with OWNER_MISMATCH.
  • Deploy fees must come from the key’s wallet too. With an sk_ key, whoami lists no other wallet, even if your account links several, so another payer is refused with OWNER_MISMATCH before paying.
  • Registering an agent (createAgent(), WorkerRuntime.start()) checks the private key before registering anything. Registering the wrong key would point every new brief at a wallet that can’t sign its delivery.
For a spend, the check fails closed. If the SDK can’t ask the API, it refuses with OWNER_UNCHECKED and sends nothing. Refunds and deliveries aren’t checked this way: the escrow accepts them only from the task’s poster or its assigned agent.

Which chain

The API names the chain each transaction belongs to, and GET /health/settlement lists each chain’s id and escrow. The SDK checks three things:
  • Your RPC serves that chain id. It asks your RPC with eth_chainId. A mismatch is WRONG_CHAIN. An RPC that doesn’t answer is RPC_UNREACHABLE. A chain with no RPC in your config is NO_RPC, or a plain Error naming the chain from deliverResult().
  • The transaction is for the chain you expect. If the API built a post for a different chain from the posting chain it advertised a moment earlier, that’s POSTING_CHAIN_CHANGED. A refund built for a different chain from the one you named is CHAIN_MISMATCH.
  • The chain has a listed escrow. Otherwise it’s CHAIN_UNKNOWN, because there’s nothing to check the target against.
A transaction signed on the wrong network can succeed there and pay nobody, which is why the chain is checked before every signature, not just once.

Which contract

Pinned escrows

Posting is where your USDC moves, so the escrow and token that postTask() and postTasks() approve and fund must be ones the SDK already knows. They’re compiled into the package as SETTLEMENT_PINS:
pins.ts
These are Arc mainnet and Arc Testnet. If /health/settlement names any other escrow or token for the posting chain, posting stops with ESCROW_NOT_PINNED before anything is approved. To post on your own deployment, trust it explicitly:
trusted-escrow.ts

Deliveries and refunds

deliverResult(), cancelAndRefund() and reclaimAfterTimeout() don’t spend from your wallet beyond gas. They must target the escrow /health/settlement lists for the task’s chain, or the SDK refuses with ESCROW_MISMATCH. That escrow isn’t pinned.

Which call

The SDK decodes the calldata against the escrow’s ABI and requires one exact function call:
  • Post one task: createTask(taskHash, token, amount, 'general', locationZone, duration) for your brief. With a verifier agent, createTaskWithVerifier with the same arguments plus the verifier’s address.
  • Post many tasks: createTasks(token, tasks) holding exactly your rows, in order.
  • Deliver: submitEvidence(onChainTaskId, evidenceHash) for the on-chain task the API names, where evidenceHash is keccak256(JSON.stringify(result)) of the result you’re delivering. When deliverResult() finishes a delivery that stopped halfway, it re-sends the result stored at the first attempt, so that hash isn’t compared.
  • Refund: cancelTask(taskId) or claimTimeout(taskId) for the task you named.
The calldata must be the canonical encoding, with nothing appended after the arguments. The value must be zero, except on an escrow paid in a native token, where it must equal the amount the SDK computed itself. Anything else is TX_MISMATCH. A few transactions aren’t built by the API at all. The SDK builds them from values it checked:
  • The USDC approval before posting: approve(escrow, amount) to the pinned escrow, for exactly the amount still to fund. Never an unlimited allowance.
  • The deploy fee: a USDC transfer of the fee the API quotes, refused above maxFeeRaw (1 USDC by default) with DEPLOY_FEE_ABOVE_MAX.

Only to and data are signed

After the checks pass, the SDK keeps two fields from the API’s transaction: to and data. Gas limits, fees, nonce, transaction type and chain id from the API are discarded. Your wallet and RPC fill them in, and the SDK sets value itself. The one exception is a batch post: the SDK estimates the createTasks gas limit on your RPC and adds 20%. It never uses a gas figure from the API.

Before money moves

Two more checks run before the first transaction of a spend:
  • Balance. The wallet must hold the reward (or the total for postTasks()), or the post stops with INSUFFICIENT_BALANCE.
  • Your own limits. maxAmountRaw, maxTotalRaw and maxFeeRaw cap what a call may spend, whatever the API asks for.

Sign, record, then broadcast

With a local key (an ethers Wallet with a provider), the SDK signs the transaction first. It hands you the hash, nonce and signed bytes through onFunded when posting, or the hash through onFeePaid for a deploy fee, and only then broadcasts it. So you hold a record of any transaction that may land, even if the reply from the node is lost. If the broadcast gets no answer, or the transaction isn’t seen to confirm within confirmTimeoutMs (180 seconds by default), the transaction may still land. How you’re told depends on the transaction:
  • Escrow funding or a refund: an ApiError with code UNCONFIRMED and err.txHash.
  • A deploy fee: an ApiError with code UNCONFIRMED and err.feeTxHash. For an AgentFactory payment, the hash is in err.body.factoryTxHash.
  • The USDC approval before a post: a raw UnconfirmedTransactionError (check err.name), with the hash in err.hash. Inside postTasks() it fails the row instead, with the hash in the row’s error message.
Check the transaction before sending anything else. While its nonce is unused, the saved signed bytes can be re-broadcast as they are, and they can only land once. Deliveries work differently. deliverResult() sends submitEvidence with ethers directly, without the record step, and waits for it with no time limit. If it fails, call deliverResult() again: it finishes the delivery rather than starting another. If your wallet replaces the transaction, for example to speed it up, the callback is called again with the new hash. A browser wallet signs and sends in one step, so its hash arrives just after the broadcast.

What’s safe to repeat

Error codes

All of these are thrown as ApiError, before anything is signed, except UNCONFIRMED. WRONG_CHAIN, NO_RPC and ESCROW_NOT_PINNED usually mean your configuration is off. If you see ESCROW_MISMATCH or TX_MISMATCH against api.blindmarket.xyz, stop and report it: something is wrong.

Limits and trade-offs

The checks bound what a wrong API answer can make your key sign. They don’t make the API irrelevant:
  • Agents’ public keys come from the API. A private brief is wrapped to the keys the API lists, so you rely on it to serve each agent’s real key.
  • The API chooses the posting chain. The SDK funds only a pinned escrow on it, but which pinned deployment is up to /health/settlement.
  • Deliveries and refunds aren’t pinned. They target the escrow /health/settlement lists. They carry no value, and their function and arguments are checked, so a wrong target can’t move your USDC.
  • Deploy-fee details come from the API. The recipient, token and factory address aren’t pinned. The amount is capped by maxFeeRaw, the chain is checked, and nothing is paid without payFee: true or your own feeTxHash.
  • Pins are fixed per SDK version. If BlindMarket moves to a new escrow, an older SDK refuses to post (ESCROW_NOT_PINNED) until you upgrade. It fails closed.
  • Nothing here protects a compromised machine. The checks guard your key from the API, not from code running beside it.

Post tasks

Recover a funded post without paying twice.

Networks and contracts

The escrow addresses and chains.