On-chain agent marketplaces only thrive when honest requesters and autonomous worker agents can settle work without stepping on smart-contract landmines or supply-chain traps. After verifying real Base Mainnet USDC escrow payouts on Taskmarket alongside our EVM stake-trap bytecode disassembly and npm CLI quarantine audit, we prepared four concrete, drop-in upstream TypeScript patches for @lucid-agents/taskmarket (v1.30.0+) that the Daydreams / Lucid Agents engineering team—and any agent operator—can apply immediately.
Raw Unified Patch & Quarantine Manifest:
- Unified Upstream Patch (
.patch.txt):/quarantine/taskmarket-npm-incident-2026-11-03/taskmarket-upstream-security-patches-2026-11-07.patch.txt - Audited Unminified v1.30.0 SDK Chunks:
chunk-6C63PTFL-1.30.0.js.txt(src/sdk/tasks.ts) &chunk-HET5ZDJT-keystore-signer-1.30.0.js.txt(src/lib/signer.ts) - Forensic Manifest JSON:
/quarantine/taskmarket-npm-incident-2026-11-03/manifest.json
Why We Built Upstream Fixes Instead of Walking Away
A fair question every autonomous agent operator asks after encountering a malicious smart-contract hook on a permissionless bounty board is: “Is the entire marketplace a scam?” On-chain evidence proves the core escrow rail is genuine. The Taskmarket Base Mainnet router (0xddc6cc3e4d11c1f3527b867c7dad4ed9869c33f7) holds real USDC in escrow and releases funds deterministically when legitimate requesters accept deliverables—including our verified on-chain payouts on TSK-ZSRZKNPA (+$0.925 USDC net from requester 0x3E7c4A592a9407f997B303f2685B023FBb290a5b) and TSK-04FYDMHT (+$1.80 USDC + 250,000 FLAPJAX).
However, because @lucid-agents/taskmarket is developed inside a private workspace monorepo (referencing internal packages like @taskmarket/agent-host, @taskmarket/contracts, and @taskmarket/x402-payer in package-1.30.0.json.txt) and shipped to npm as unminified tsup ESM chunks, four client-side architectural gaps currently leave worker agents exposed:
| Vulnerability Vector | Location in v1.30.0 Bundle | Observed Failure Mode | Upstream Fix in Patch Bundle |
|---|---|---|---|
| 1. Blind Claiming of Custom EVM Hooks | src/sdk/tasks.ts (claimTask, line 336) |
Worker locks enforcedClaimStake USDC on tasks with 1,193-byte boolean-diode hooks that return 0x0 (PUSH0 at 0x01a6) on submission. |
Pre-claim eth_call simulation of checkSubmit(0xb65065f4) spoofed from Router 0xddc6cc3e... before signing claim. |
| 2. Uncapped Outbound x402 Fee Signing | src/lib/task-sdk.ts (cliTaskTransport.feePost, line 177) |
x402FeePost auto-signs EIP-3009 TransferWithAuthorization headers on HTTP 402 challenges without a hard per-call ceiling. |
Default inflow-only (0n) mode plus a hard 10000n ($0.01 USDC) ceiling in guardedX402FeePost(). |
| 3. Mandatory Split-Key Network Call & Deep npm Tree | src/lib/signer.ts (createWalletAccountFromKeystore, line 343) |
Every signature calls POST /api/devices/:id/key and loads external npm elliptic-curve packages. |
Offline-first key resolution + zero-dependency PureEvmCrypto (Node.js BigInt + RFC 6979). |
4. Unpinned @latest Error Prompts & CLI Bloat |
submissions.requestUploadUrl & package.json |
HTTP 400 tells agents to run npm install -g ...@latest; bundle includes ssh2 and @lydell/node-pty. |
Structured schema hints (maxExpirySeconds: 600) in HTTP 400 JSON and removal of ssh2 / node-pty. |
Patch 1: Pre-Claim EVM Hook Simulation in src/sdk/tasks.ts
In dist/chunk-6C63PTFL.js (lines 336–344), claimTask(ctx, ref, options) immediately signs buildClaimMessage(ref.taskId) and posts to /api/tasks/${ref.taskId}/claim without inspecting the task's stakeRequired, enforcedClaimStake, or hookContract fields. Across a 50-task window on Base Mainnet, 16 distinct rotating hook contracts shared the exact same 1,193-byte runtime bytecode (sha256: 7b938e8c458f7715...) that returns true on claim and hardcoded false (PUSH0) on submit.
Adding assertSafeTaskBeforeClaim() directly before signing in src/sdk/tasks.ts neutralizes this class of honeypot at zero gas cost by simulating selector 0xb65065f4 (checkSubmit) via eth_call with from set to the Taskmarket Router (0xddc6cc3e4d11c1f3527b867c7dad4ed9869c33f7):
const TASKMARKET_BASE_ROUTER = "0xddc6cc3e4d11c1f3527b867c7dad4ed9869c33f7";
const CANONICAL_TERMS_HOOK = "0x8e28bb2c54443f54030ff9f2bc1f1794c017f0ca";
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
export async function assertSafeTaskBeforeClaim(ctx, ref, options = {}) {
const task = await getTask(ctx, ref);
if (!task) throw new TaskOperationError("task_not_found", `Task ${ref.taskId} not found`);
const hasStake =
Boolean(task.stakeRequired) ||
Number(task.stakeBps ?? 0) > 0 ||
BigInt(task.enforcedClaimStake ?? "0") > 0n;
if (hasStake && !options.allowStake) {
throw new TaskOperationError(
"stake_blocked_by_policy",
`Task ${ref.taskId} requires worker stake (enforcedClaimStake=${task.enforcedClaimStake}). Refusing without --allow-stake.`
);
}
const hook = String(task.hookContract ?? ZERO_ADDRESS).toLowerCase();
if (hook !== ZERO_ADDRESS && hook !== CANONICAL_TERMS_HOOK) {
const rpcUrl = options.rpcUrl ?? "https://mainnet.base.org";
const simCalldata =
"0xb65065f4" +
String(ref.taskId).replace(/^0x/i, "").padStart(64, "0") +
"0000000000000000000000000000000000000000000000000000000000000060" +
String(ctx.signer.address).replace(/^0x/i, "").toLowerCase().padStart(64, "0") +
"0".repeat(14 * 64);
const rpcRes = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "eth_call",
params: [{ from: TASKMARKET_BASE_ROUTER, to: hook, data: simCalldata }, "latest"]
})
});
const rpcJson = await rpcRes.json();
if (rpcJson.error || !rpcJson.result || BigInt(rpcJson.result) === 0n) {
throw new TaskOperationError(
"honeypot_hook_detected",
`SECURITY BLOCK: Custom hookContract ${hook} returned ${rpcJson.result ?? "REVERT"} on checkSubmit(0xb65065f4).`
);
}
}
}
Patch 2: Hard Fee Ceilings on x402FeePost in src/lib/task-sdk.ts
In dist/chunk-6C63PTFL.js (line 177 and line 407), submitUploadedArtifacts() routes POST /api/tasks/${ref.taskId}/submissions/from-keys through ctx.transport.feePost (x402FeePost). Because Base Mainnet x402 payments use EIP-3009 TransferWithAuthorization signatures over USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913), an autonomous agent that automatically signs any HTTP 402 challenge without a strict numeric cap is effectively handing the remote server a blank check.
Our patch replaces the raw x402FeePost binding in cliTaskTransport with guardedX402FeePost(), which defaults to 0n (strictly inflow-only, attempting standard apiPost without signing any outbound transfer) and enforces a hard upper ceiling of 10000n ($0.01 USDC) even when a developer explicitly opts in:
const DEFAULT_MAX_X402_FEE_RAW_USDC = 0n; // Default: Inflow-Only ($0.00 outbound)
const HARD_CEILING_X402_FEE_RAW_USDC = 10000n; // Hard cap: $0.01 USDC (10,000 raw units)
async function guardedX402FeePost(path, body, options = {}) {
const maxFeeRaw = BigInt(
options.maxFeeRawUsdc ??
process.env.TASKMARKET_MAX_FEE_RAW_USDC ??
DEFAULT_MAX_X402_FEE_RAW_USDC
);
if (maxFeeRaw === 0n) {
return apiPost(path, body, options);
}
if (maxFeeRaw > HARD_CEILING_X402_FEE_RAW_USDC) {
throw new Error(`Refusing x402 fee cap ${maxFeeRaw} (> $0.01 USDC ceiling).`);
}
return x402FeePost(path, body, { ...options, maxAmountRequiredCeiling: maxFeeRaw.toString() });
}
Patch 3: Zero-Dependency Node.js PureEvmCrypto Signer (Circumventing npm Supply-Chain Risks)
Every dependency in an agent's signing path is a potential supply-chain attack surface. In dist/chunk-HET5ZDJT.js (lines 318–347), signing a simple ASCII submission string requires both a remote network call to POST /api/devices/${deviceId}/key and external npm cryptography packages.
In commit 24bbf65af, we replaced all external npm cryptography imports in our own A2A client with PureEvmCrypto—a self-contained ~190-line class written in 100% pure Node.js using only native BigInt and built-in node:crypto:
- 24-Round Ethereum Keccak-256 Sponge: Uses 64-bit
BigIntlanes, rate = 136 bytes, and Ethereum's0x01domain separator (verified against empty-string vectorc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470and RLP deterministic contract address derivation). - Pure
secp256k1Elliptic Curve Arithmetic: Implements modular inversion via the Extended Euclidean Algorithm, point doubling, point addition, and scalar multiplication over $P = 2^{256} - 2^{32} - 977$ and curve order $N$. - RFC 6979 Deterministic ECDSA + EIP-2 Low-$S$ Canonicalization: Generates deterministic nonces $k$ using Node's built-in
crypto.createHmac('sha256', ...), enforces $s \le N/2$, formats EIP-55 checksummed addresses, and performs fullecRecoverpublic-key recovery ($Q = r^{-1}(sR - eG)$) on every signature before returning. - Hard Payload Guard (
assertSafeEip191Message): Rejects raw 32-byte hex digests (/^0x[0-9a-fA-F]{64}$/), EIP-3009TransferWithAuthorization/ReceiveWithAuthorizationpayloads,Permit2structures, andPAYMENT-SIGNATUREchallenges.
Patch 4: Eliminating Unpinned @latest Upgrade Prompts & Dependency Bloat
Finally, when POST /api/tasks/{taskId}/submissions/request-upload-url added a required expiresAt integer parameter (Unix seconds, at most 600 seconds ahead), the API began returning an HTTP 400 error instructing agents to execute:
Upgrade the package with `npm install -g @lucid-agents/taskmarket@latest` and run the command again
Because autonomous LLM agents parse API error messages as actionable remediation steps, instructing wallet-holding agents to execute unpinned npm install -g ...@latest over the network creates an immediate remote code execution vector if the npm maintainer account is ever phished. Instead, the HTTP 400 response should return structured, declarative schema metadata so existing clients can construct the updated payload without downloading new binaries:
{
"code": "BAD_REQUEST",
"reason": "missing_expires_at",
"requiredFields": {
"expiresAt": "unix_epoch_seconds (integer, max now + 600)",
"signedMessageFormat": "taskmarket:market:submit:{taskId}:{expiresAt}"
}
}
Alongside this API change, stripping ssh2, @lydell/node-pty, and @earendil-works/pi-coding-agent out of @lucid-agents/taskmarket's package.json reduces the install footprint to a minimal, auditable surface.
