Skip to content

Served API spec offers only the networks a buyer can pay on; externalDocs; Bazaar output schema - #187

Merged
maxfain merged 40 commits into
mainfrom
claude/spec-implementation-dkc77z
Oct 9, 2026
Merged

maxfain merged 40 commits into
mainfrom
claude/spec-implementation-dkc77z

Conversation

@maxfain

@maxfain maxfain commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Circle's marketplace reviewers, its readiness check and buying agents all read /openapi.json. Since #183 it told buyers they could pay "on Base, Polygon or Arc". Production offers Arc only once CIRCLE_API_KEY is set, and that key isn't set. The hire request schemas also listed Base Sepolia and Arc in their network enum, which production refuses with 400 bounty_network_not_allowed. Circle's services inspect builds probe bodies from that schema, so some probes would pick a network we refuse.

Changes

The spec is served with the networks offered here. openApiForEnv already adjusted prices to the live minimum. It now also narrows the hire endpoints to fundableBountyNetworks(env):

  • the network enum of X402TierHireRequest and X402HireRequest;
  • the networks example of the price list;
  • the network wording in info.x-guidance and the hire operations' descriptions, in the file's three phrasings ("Base, Polygon or Arc", "Base eip155:8453, Polygon eip155:137 or Arc eip155:5042", "Base, then Polygon, then Arc").

What production serves:

Circle key set? Served networks
No (today) Base and Polygon
Yes Base, Polygon and Arc

A testnet is never served in production. Off production, Base Sepolia stays. Other operations keep their wording, for example /v1/tasks, whose text already qualifies Arc. The file itself is unchanged.

externalDocs. The spec links the runbook, https://basedagents.ai/skill.md. Circle's readiness page gives a docs link 5 points (M("docsUrl", a.docsUrl, 5, …) in its score bundle). Our spec had none, and we scored 95/100.

The Bazaar block declares its output. extensions.bazaar.schema.properties.output is now {type: object, properties: {type: string, example: object}, required: [type]}, as the reference declareDiscoveryExtension emits it. Circle's catalog copies some sellers' response schema from this block.

Checked

  • CDP's validator. The free, read-only POST https://api.cdp.coinbase.com/platform/v2/x402/validate with method: POST returns valid: true and simulation.outcome: accepted for all four hire URLs on the current production. CDP documents it as making no payment and indexing nothing. So Bazaar listing: forward our own resource and Bazaar block to the facilitator #184's forwarded Bazaar block is not at risk of invalid_bazaar_extension.
  • The new block. The live 402 with the output schema added passes the reference validateDiscoveryExtension, validateDiscoveryExtensionSpec, validateAndExtract and extractDiscoveryInfo (@x402/extensions 2.28.0). A broken output.example is rejected, so the schema is enforced.

Tests

  • openapi.test.ts:
    • the file contains the three wordings that are rewritten;
    • production without the key serves no "Arc" or eip155:5042 in x-guidance or the hire operations, and [8453, 137] enums and example;
    • production with the key serves the file's wording and [8453, 137, 5042];
    • off production, Base Sepolia stays and Arc goes;
    • /v1/tasks and the file are untouched;
    • externalDocs.url is set.
  • x402-tasks.test.ts: the 402 declares the output schema.
  • Results:
    • API 1281 passed.
    • Typecheck clean; lint 0 errors (1 pre-existing warning).
    • Positioning, front-door self-tests, sync-skill and examples pass.
    • The web build passes.

After merge

  • Re-run the CDP validator on the four hire URLs.
  • Confirm the served /openapi.json names only Base and Polygon.

Docs

SPEC (GET /openapi.json and the price-check bullet) and CHANGELOG.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9


Generated by Claude Code

The hero's proof panel gets a second figure, styled like "Total paid to
agents": "Registered agents" with the live count from GET /v1/status
(agents.total), "All time · On the public registry", and a link to the
agent directory. Like the paid total it prerenders as a dash and fills in
after hydration; an unreachable API shows a dash, never 0.

The homepage already read /v1/status for its live-work section; both now
share one request (lib/registryStatus.ts, reused for a minute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Review on #156: registryStatus (lib/registryStatus.ts) now has tests for
two callers sharing one request, reuse for a minute then a fresh fetch,
and a failed request not being kept so the next caller retries. The web
package gets a vitest test script (same vitest as the other packages), so
`npm test` runs it in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Circle agent wallets are smart-contract accounts on Base that stay
counterfactual until their first transaction, so their signatures come
ERC-6492-wrapped. The wallet bind used to refuse those with
undeployed_smart_wallet; it now checks them.

- api: an ERC-6492 signature is verified with the reference
  ValidateSigOffchain bytecode run as a deployless eth_call (no `to`),
  which deploys the wallet in the call, then asks it per ERC-1271. A
  0x01 result binds (recorded as signer_kind erc1271); anything else or
  a revert is bad_signature; an unreachable RPC is rpc_unavailable.
  The bytecode is pinned by sha256 in the tests. wallet_proof.signature
  accepts up to 8192 bytes, since a 6492 wrapper carries the factory
  calldata.
- sdk 0.9.6: `wallet set` without a key also prints the Circle CLI
  command that signs the exact message (`circle wallet sign message
  0x<hex> --hex --address … --chain BASE`), and --json carries it as
  circle_sign_command.
- skill 1.3.7 (min CLI 0.9.6), SPEC, OpenAPI, READMEs, CHANGELOG.

Checked live on Base mainnet against an undeployed smart account: its
6492 signature binds; a changed message, another wallet's claim and a
flipped signature byte are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…yed"

Two review findings on the Circle wallet change:

- The service descriptor's walletProof.smartWallets still said "deployed
  wallets on Base", contradicting the skill. It now says a wallet not
  deployed yet signs per ERC-6492; the .well-known copies are regenerated.
- Whether `circle wallet sign message` signs from a counterfactual wallet
  is not verified. If it answers "Wallet not deployed", Circle's
  documented fix is a zero-amount transfer to itself, after which the
  wallet is deployed and binds per ERC-1271. `wallet set` now prints that
  command next to the sign command (circle_deploy_command in --json), and
  the skill, SDK README and CHANGELOG say when to run it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9

# Conflicts:
#	CHANGELOG.md
#	package-lock.json
#	packages/sdk/package.json
#	packages/web/public/llms-full.txt
A real Circle agent wallet bind showed three problems:

- Circle's CLI signs only from a deployed wallet ("This wallet isn't
  deployed on-chain yet"). `wallet set` now prints the deploy step first
  for Circle wallets; skill 1.3.8 §3 and the SDK README say so. Once
  deployed, the wallet binds per ERC-1271.
- `circle wallet transfer … --token usdc` fails with 404 "Cannot find
  target token": --token takes a contract address and defaults to USDC.
  circle_deploy_command no longer passes it.
- The API checked smart-wallet signatures against mainnet.base.org alone,
  which answered 429 from Cloudflare's shared egress and failed the bind
  with 503. It now tries BASE_RPC_URL (one URL or a comma-separated
  list), then mainnet.base.org, base-rpc.publicnode.com and base.drpc.org
  (same operators on Sepolia), each checked live for eth_getCode, eth_call
  and the deployless ERC-6492 call. A 429, 5xx, timeout or unsupported
  method moves on; a revert is the wallet's answer and stops there.
  Per-attempt timeout 5s.

basedagents 0.10.1, skill 1.3.8 (min CLI 0.10.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Review finding: with up to four endpoints, 5 s per attempt and two calls
(eth_getCode, eth_call), one proof could spend 40 s on RPC, past the
SDK's 30 s request timeout, so a bind that succeeded on a late endpoint
could be abandoned by the client.

All RPC attempts for one proof now share a 15 s deadline (each attempt
gets min(5 s, what's left)), and the endpoint that answered eth_getCode
goes first for eth_call. A test with every endpoint hanging shows the
check returning rpc_unavailable at 15 s after three attempts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Review finding on 787079e: with a shared 15 s deadline tried one endpoint
at a time, three nodes that each hang for 5 s use the whole budget, so a
healthy fourth is never tried, and a late eth_getCode can leave no time
for eth_call. Either way a valid bind answers rpc_unavailable.

Each call now starts on the first endpoint and also goes to the next one
on any failure at once, or after 1.5 s of silence (a hedged request). The
first answer wins and the others are cancelled; a revert still wins as
the wallet's answer. Each call has its own 9 s budget, so a proof (at
most two calls) settles within 18 s, inside the SDK's 30 s. The endpoint
that answered first goes first for the next call.

Tests: a hanging configured node is passed over after 1.5 s and the bind
succeeds on the next; with every node hanging, all four are tried and
the call gives up at 9 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Review finding on d9c1f1e: with hedged first-answer-wins, a public node
that hasn't seen a wallet deployed seconds ago can answer eth_getCode
"0x" before a slower, up-to-date node, and the bind is refused as
bad_signature. That is exactly the Circle flow: deploy, then bind.

Lag only ever hides a yes, so the verdict is now asymmetric: a yes from
any node wins at once, while a no (wrong answer, no code, a revert)
counts only when every endpoint has answered, or the budget ran out, and
none said yes. Failures still move on at once and 1.5 s of silence still
hedges to the next node.

The separate eth_getCode is gone: eth_call to isValidSignature on an
address with no code answers empty, which is a no, so one call (9 s
budget) does the whole check.

Tests: a slow configured node with the wallet beats three public nodes
answering "no code" (would fail before); a revert on one node loses to
a yes on another; a no needs all three public nodes. Live on Base
mainnet: the ERC-6492 vector still binds, a flipped byte, an address
with no code and a contract without isValidSignature are bad_signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Review findings on 21f611d (both security):

1. Without eth_getCode, the prefix check on isValidSignature's result
   accepted the identity precompile (0x…04): it echoes the call data,
   which starts with 0x1626ba7e, so an agent could "verify" it.
2. "A yes from any node wins" let a lagging node approve with a signer
   the wallet has since removed.

Lag runs both ways, so neither a lagging yes nor a lagging no can be
trusted; the fix removes the lag instead:

- Every smart-wallet signature, deployed (ERC-1271) or not (ERC-6492),
  is checked by the ERC-6492 reference validator in one deployless
  eth_call. It answers exactly 0x01 only for a valid signature: it
  asks isValidSignature when the address has code and recovers the
  signer when it has none. With no `to`, no precompile or other
  contract can answer in the wallet's place.
- The call is pinned to the highest head block the nodes report
  (eth_blockNumber to all at once, answers within 1.5 s of the first
  count). Every node that has that block gives the same answer, so
  the first answer wins; one that lags errors and is skipped.
- Head lookup 3 s, check 9 s: inside the SDK's 30 s.

Checked live on Base mainnet on all three public nodes, with a state
override standing in for a deployed wallet: a wallet answering the
ERC-1271 magic -> 0x01, one answering 0xffffffff -> 0x00, precompile
0x04 -> 0x00, a block the node lacks -> error. End to end through
verifyBindProof: the ERC-6492 vector binds; a flipped byte, 0x…04, an
address with no code and USDC are bad_signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Review finding on 66137fc: if the node that reported the freshest block
fails the eth_call (say it's rate limiting us) while the healthy nodes
are a block behind, every node fails at the pinned block and a valid
bind answers 503.

Falling back to an older block would bring back stale answers, so
instead: when every endpoint has failed at the pinned block, the round
starts over at the same block after 2 s (one Base block), by which time
the nodes behind have it. Rounds repeat within the 9 s budget.

Test: mainnet.base.org reports 0x12 and answers 429; the other nodes
report 0x11 and say "block not found: 0x12" on the first round. The bind
succeeds on the second round, and every call is pinned to 0x12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9

# Conflicts:
#	CHANGELOG.md
The first real Circle wallet bind hit `zsh: command not found:
basedagents`: the `Then run:` line printed a bare `basedagents wallet
set …`, but run through npx (the way the docs run it) nothing is
installed. The same was true of every command the CLI prints to run
next: tasks claim/deliver/payment/fund/watch/accept/dispute hints,
register, init, validate and id.

cliCommand() (cli/invocation.ts) answers `npx basedagents@latest` when
the CLI was started through npx (npm sets npm_command=exec and runs the
package from its _npx cache) and `basedagents` when it's installed.
The 24 run-this hints, and the --json `next` field, use it; title
banners and help text keep the bare name.

Tests: cliCommand for npx (both signals, POSIX and Windows cache
paths) and installed runs; `wallet set --json` under npx gives an npx
`next`. The wallet and tasks tests pin npm_command so they pass however
vitest is launched. Checked by running the packed 0.10.2 tarball
through a real npx: `Then run: npx basedagents@latest wallet set …`.

basedagents 0.10.2 (also publishes #163's SDK attribution, merged
without a version bump).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9

# Conflicts:
#	CHANGELOG.md
packages/mcp/dist is committed so the server runs from a checkout
(bin/basedagents-mcp.mjs imports ../dist/index.js). #163 changed the MCP
source without rebuilding, so a checkout ran the pre-attribution server.

Regenerated with `npm run build --workspace=packages/mcp`, and added the
new dist/attribution.{js,d.ts} and their maps with `git add -f`: the new
index.js imports ./attribution.js and dist/ is gitignored. npm publishes
were never affected (prepublishOnly rebuilds).

Checked from a `git archive` of the committed tree: the stdio server
initializes as basedagents 0.7.2 and lists 26 tools; the archived
index.js is byte-identical to a fresh build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
A buyer without an agent key or account can now post a task: the x402
escrow deposit is the authentication, and the paying wallet is the poster.

- POST /v1/x402/tasks takes a buyer-chosen bounty_usdc (at least the
  minimum); /usd-1, /usd-5 and /usd-20 take a fixed price. An empty POST is
  a price check (402, nothing written); the 402 carries a Bazaar discovery
  block. GET /v1/x402/tasks lists the prices.
- The poster is the payer the facilitator verified (creator_kind 'wallet',
  creator_wallet), never a request field.
- Manage by the token returned once (only its sha256 is stored) or by a
  signature from the paying wallet over a one-time action message
  (X-Wallet-Message + X-Wallet-Signature; the 401 gives the message and the
  Circle sign command). Accept, revision, dispute and cancel run the same
  gates as the owner routes.
- Binding the wallet as an agent's payout wallet moves its tasks to that
  agent in the same batch; the wallet keeps access. GET /v1/tasks?creator=0x…
  lists a wallet's tasks.
- Migration 0051 rebuilds tasks (creator_kind 'wallet', creator_wallet,
  manage_token_hash) and adds wallet_action_nonces.
- OpenAPI, /.well-known/x402, /docs, the service descriptor, SPEC, README
  and CHANGELOG document it; the MCP server shows a wallet poster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
… branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…on footers, tier minimum

- The poster's private reads (GET /v1/x402/tasks/:id and /submission) send
  Cache-Control: no-store, and the cache default counts X-Wallet-Signature as a
  credential, so a wallet-signed read is never marked public.
- Web (task detail, board card) and console (Explore) show a wallet poster as
  "a wallet · 0x1234…abcd" instead of "an agent"; the creator types gain
  kind 'wallet' and `wallet`.
- The signed action message's last line now says what the action does with the
  escrowed bounty: accept releases it, cancel refunds it, revision and dispute
  leave it held. The parser requires the footer of the named action.
- A tier priced under the live minimum bounty is refused before any quote
  (400 bounty_below_minimum, no 402) and left out of both price lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…idance, contact)

Circle's agent-readiness check found no paid endpoint declared in
openapi.json. The four wallet-only hire operations now carry x-payment-info
(fixed 1/5/20 USDC on the tiers; dynamic 0.10–1000 on the custom endpoint;
protocols [{x402: {}}]). info.x-guidance replaces the non-standard
info.guidance with a short agent brief, and info.contact gains
hello@basedagents.ai. Tests pin the priced set, the tier amounts against the
route's prices, and route/spec parity for /v1/x402/tasks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
The cancel route refuses submitted work until it has been disputed; the guidance
listed cancel as a direct option on a delivery, which would earn an agent a 409.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
GET /openapi.json now passes the spec through openApiForEnv: the custom hire
endpoint's price.min is the deployment's minimum, and a tier priced under it
is left out (the route refuses it before any 402). With the default minimum
the file is served unchanged. x-guidance points to GET /v1/x402/tasks for the
live prices instead of pinning 0.10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…adata branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
… any chain

Circle's readiness check asks a paid endpoint to accept 2+ networks. USDC
bounties can now be paid on Polygon PoS (eip155:137, native USDC) through the
same CDP facilitator and the same escrow.

- The wallet-only hire 402 offers the same price on every accepted network
  (production: Base first, then Polygon); payTo is the escrow wallet, the same
  address on each. The payer signs one offer and the task's bounty network is
  the one they paid on: held, released and refunded there, nothing bridged.
  A body `network` restricts the offer. Agents and owners may post
  bounty.network eip155:137.
- payoutWallet (payments/accept.ts) decides where a deliverer is paid on a
  bounty's network: a wallet bound there, or a verified plain-key wallet's
  same address on any EVM chain. It runs at claim (409
  wallet_network_mismatch) and again at every payout (escrow release deferred
  with payee_wallet_wrong_network; sign-at-accept 409), so switching wallets
  after claiming can't route a payout to an address that isn't the agent's.
- Claim bonds are deposited on Base and now always paid back on Base (to a
  Base wallet or a plain key's address); before, a withdrawal followed the
  payout wallet's network, which would have drawn on Polygon escrow funds.
- Polygon USDC's EIP-712 domain (USD Coin, 2) was read on-chain and pinned;
  POLYGON_RPC_URL plus public nodes check smart-wallet signatures; the paid
  feed links Polygonscan; the descriptor lists payments.networks; the Circle
  sign command uses --chain MATIC.
- SDK 0.10.3: BOUNTY_NETWORKS includes eip155:137; Circle command for MATIC.
- OpenAPI enums and copy, SPEC, README, CHANGELOG; tests for the multi-network
  402, Polygon deposits and payouts, the plain-key rule, the deferred release,
  and Base-only bond payouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…ion; review fixes

- GET /v1/x402/tasks/usd-{1,5,20} answers the same 402 quote as an empty POST.
  It used to fall through to GET /:id and 404, so `circle services inspect`
  (GET by default) showed the service as unavailable. A GET carrying a payment
  is refused (405, Allow: POST) and the payment is not used.
- PaymentPayloadV2 keeps `extensions`: a client echoes the 402's bazaar block
  into its payment and a facilitator catalogs the service from it at settle.
  The parser dropped it, so it never reached CDP. Forwarded on verify and on
  settle (from the stored header).
- Greptile: the price list's OpenAPI response documents `networks` (array)
  instead of `network`; the settled-feed tests cover a Polygon settlement
  (Polygonscan link, counted in the totals) and explorerTxUrl for 137.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…wallet

USDC bounties can be paid on Arc (eip155:5042), Circle's stablecoin chain,
alongside Base and Polygon. CDP doesn't settle on Arc, so paymentProviderFor
now routes by network (NetworkRouter): Base and Polygon to CDP, Arc to
ArcFacilitator, which splits by who signed the transfer.

- Deposits (buyer -> escrow wallet) verify and settle through Circle's
  Facilitator Service (circle-facilitator.ts, Bearer CIRCLE_API_KEY), with a
  payment-identifier from the authorization nonce. Circle's pending answer
  has no tx hash: SettleOutcome 'pending' now allows that, and settle.ts keeps
  a hash an earlier attempt saw. invalid_transaction_state = nonce used.
- Payouts and refunds (escrow wallet -> agent/buyer) never go to Circle,
  which would bind the payee's wallet to our Circle account. The escrow
  wallet broadcasts its own transferWithAuthorization (arc.ts relay): checks
  authorizationState first (a used one resolves to the tx in its
  AuthorizationUsed log), one escrow-wallet tx in flight at a time, simulates,
  pays 2x base fee + tip (>= Arc's 20 gwei floor), checks the USDC float for
  transfer + gas, waits 6 s for the receipt, else pending with the hash.
- EIP-1559 signing and RLP live in a leaf module (evm.ts) with the key
  parser; house-wallet re-exports, so there is no import cycle. The signer
  matches eth-account byte for byte (vectors pinned in evm.test.ts).
- New Arc deposits are offered only while CIRCLE_API_KEY is set
  (fundableBountyNetworks); existing Arc tasks stay visible and payable.
  Arc bounties are escrow-only (400 escrow_required on escrow: false).
- USDC on Arc: 0x3600...0000, EIP-712 domain USDC/2 read on-chain; the
  offer's extra carries assetTransferMethod: eip3009 for Circle.
- Payout wallets bind on Arc (ARC_RPC_URL + Arc's public nodes); the paid
  feed and task page link explorer.arc.io; Circle CLI chain ARC; SDK 0.10.4.
- Docs: SPEC (Arc section, custody, env, network table), OpenAPI enums and
  copy, SECURITY, API README, deploy runbook step 8, CHANGELOG.

Checked live on Arc mainnet with a throwaway key: the escrow code's
authorization passes USDC's signature check (reverts only on balance), a
flipped signature byte fails it, and the node decodes the signed tx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
claude added 10 commits October 7, 2026 22:45
…d old logs

Review fixes (Greptile on #183):

- P1 double payment: the relay called an authorization expired 10 s early by
  its own clock, before looking at a transfer still in flight; settle.ts then
  put the task back to funded and the cron could sign a second transfer while
  the first one could still land. Expiry is now the chain's call only: once a
  block at or past validBefore exists and the authorization is unused AT that
  block (eth_call pinned to it, so a lagging node errors instead of answering
  from older state), it can never be used, and only then is the leg expired.
  A simulation's "expired" waits for the chain the same way.
- P1 concurrent payouts sharing a nonce: every round now starts from the chain
  (used? expired?), waits up to 4 s for any escrow-wallet transaction in flight
  to land, and sends on the next nonce; a send that loses the nonce to a
  concurrent payout, or whose nonce goes to another transaction while it is
  watched, starts over in the same call. A revert after a clean simulation is
  retried later with backoff instead of being resent at once.
- P2 late log lookups: the AuthorizationUsed search now finds the validity
  window's blocks by timestamp (interpolation search) instead of estimating
  back from the head, so a retry a day later still finds the transaction.
- P2 fake node: FakeArcNode now keeps a mempool (pending vs latest nonce),
  changes authorization state only when a transaction is mined (unused and
  unexpired at that block), refuses a taken nonce like a node does, gives
  every block a timestamp, honours getLogs ranges (5,000-block cap) and errors
  on a pinned read it can't serve.

Tests: the in-flight payout regression through the real settle machine (it
fails with the old expiry check), expiry by chain clock vs ours, a dropped
transfer expiring only at the pinned block, waiting for and racing another
payout (two Workers land on consecutive nonces), a lost nonce, and finding the
log a day later in two queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9

# Conflicts:
#	CHANGELOG.md
… verdict

Review fix (Greptile on #183, P1): on a leg's first attempt settle.ts's row
says nothing was ever broadcast, so it reads "nonce already used" or a
terminal verdict as "this authorization never moved money" and lets the
cron sign a new one. If the relay's own transfer landed in that same call
but its receipt and its AuthorizationUsed log couldn't be read (watch() saw
the nonce move on and called it lost; the next round found the
authorization used with no log), the relay answered nonce_already_used and
the bounty could be paid twice.

The relay now remembers the transaction it sent (set before the broadcast,
since a node may hold it whatever the call answers). After that, a used
authorization with no readable transaction is `pending` with that hash, and
a simulation's terminal verdict waits for the next attempt, where settle.ts
knows a broadcast happened. The pinned-block expiry stays: unused at a block
past validBefore can never be used.

Tests (all three fail without the fix): the relay answers pending with its
hash when receipts and logs are unreadable, and the reuse answer only before
any send; a terminal simulation verdict after a send waits; and through the
real settle machine, such a payout stays releasing (the sweep doesn't
re-sign) and resolves to the transfer it recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…hout a hash

Review fix (Greptile on #183, P2): after a send in the same call, keepOpen
answered a used authorization whose transaction couldn't be read with
`pending` carrying the hash this call last sent. That hash may never have
landed: e.g. an earlier attempt's transfer lands just as the retry
broadcasts (other fees, so another hash), the node refuses the retry with
"nonce too low", and logs can't be read. settle.ts then replaced the
recorded hash (the transfer that did land) with the refused one, and a
later reuse answer settled the payout with a link to a transaction that
never existed on-chain.

Now the call tracks only whether it broadcast, and answers `pending` with
no hash when the using transaction is unknown, so whatever settle.ts
recorded stays; the next attempt reads the log and records the real one.

Tests: the race above (fails on the previous relay); the unreadable-receipt
case now expects a hashless pending; through the settle machine the payout
resolves to the transfer that landed once the chain can be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…l over

Review fix (Greptile on #183, P2): with the previous change, a payout that
landed on its first attempt while receipts and logs were unreadable came
back as pending with no hash; if logs were still unreadable on the next
retry, that call (having sent nothing) gave the reuse answer and settle.ts
settled the payout with no transaction hash, permanently: no explorer link,
and out of the paid feed and totals.

findAuthorizationTx now tells "absent" (every block of the window was read,
no log) from "unreadable" (a node refused a query, a block couldn't be
read, or the window needed more queries than allowed). Unreadable answers
retry-later, so the leg stays open until its transaction can be read (the
cron's 24 h unknown-outcome cap still hands a stuck row to a human); only
absent gives the reuse answer. eth_getLogs now moves to the next endpoint
when a node refuses the query, instead of taking the refusal as final.

Tests (the three below fail on the previous relay): a landing with
unreadable receipts and logs stays open over two attempts and settles with
its transaction once logs are readable; a refused log query is answered by
the next endpoint; through the settle machine the payout stays releasing
through a second retry with logs still unreadable, then releases with the
transfer that landed. The keepOpen race now uses a window that reads clean
of the log (FakeArcNode.hideLogs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…litator

CDP's Bazaar lists a service from the `resource` and `extensions.bazaar`
that a payment echoes from the 402. Circle's CLI (`circle services pay`,
@circle-fin/cli 1.2.0) builds its payment from the signed transfer,
`accepted` and `resource` only, so the first paid hire settled with no
Bazaar block and the escrow wallet's listing stayed empty.

The payload we verify, store and settle now carries the 402's `resource`
and the `bazaar` block the endpoint declared, whatever the payer echoed
(payloadForFacilitator). A payer can't keep a hire endpoint unlisted or
list it under another URL or description; their other extensions pass
through. The sign-at-accept transfer and claim bonds get their 402's
resource the same way, and a `bazaar` block a payer adds there is dropped.
decodeForFacilitator refuses a payment our declaration would push past the
16 KB header cap, so settle can always decode what was stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
…Docs; Bazaar output schema

Circle's marketplace reviewers and buying agents read /openapi.json. Since
the Arc release it told buyers they could pay "on Base, Polygon or Arc",
but production offers Arc only once CIRCLE_API_KEY is set, and the hire
request schemas listed Base Sepolia and Arc in their `network` enum, which
production refuses with a 400.

- openApiForEnv serves the hire endpoints with only the networks a deposit
  can be paid on here (fundableBountyNetworks): the request `network` enums,
  the price list's example, and the network wording in info.x-guidance and
  the hire operations. Production without the Circle key names Base and
  Polygon; with it, Arc too; never a testnet.
- externalDocs links the runbook (https://basedagents.ai/skill.md), the docs
  link Circle's readiness check scores.
- The 402's Bazaar block declares its response envelope
  (schema.properties.output) as the reference declareDiscoveryExtension
  does. It passes the reference validators; CDP's validator accepted the
  hire endpoints before the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VX2Nm8D11UusvYsuAtrcY9
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

🔎 Console preview: https://92735c36.basedagents-console.pages.dev
🌐 Site preview: https://f73d27d1.auth-ai-web.pages.dev

(Passkey ceremonies only work on app.basedagents.ai — previews are for UI review.)

@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Critical impact] The PR appears safe to merge; no actionable issues were found.

Summary

The served API spec now matches the networks available for hiring in each deployment.

  • Hire docs show only payment networks this deployment offers.
  • The API spec points readers to the buyer and agent runbook.
  • Hire payment quotes declare the shape of their response.

No actionable issues were found. This review checked source code and tests; it did not run tests or external validators.

Reviews (1) · Last reviewed commit: "Served API spec offers only the networks..." · Reviewed by Greptile

@maxfain
maxfain merged commit 809c632 into main Oct 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants