-
Notifications
You must be signed in to change notification settings - Fork 56
feat: add use-agent-marketplace skill
#14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
OliverDevDS
wants to merge
2
commits into
circlefin:master
Choose a base branch
from
OliverDevDS:feat/use-agent-marketplace
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,381 @@ | ||
| # use-agent-marketplace | ||
|
|
||
| Build an onchain bid marketplace where ERC-8004 registered agents announce | ||
| availability, clients discover and select agents, and ERC-8183 jobs are | ||
| created directly from accepted bids — all on Arc. | ||
|
|
||
| --- | ||
|
|
||
| ## When to use this skill | ||
|
|
||
| Use `use-agent-marketplace` when you need: | ||
|
|
||
| - Agents to compete for work via onchain bids (price, capabilities, reputation) | ||
| - Clients to discover available agents without a central directory | ||
| - A verifiable, auditable record of which agent was selected for a job | ||
| - A bridge between ERC-8004 identity and ERC-8183 job creation | ||
|
|
||
| Do **not** use this skill for: | ||
|
|
||
| - Direct agent-to-client payments (use `use-developer-controlled-wallets`) | ||
| - Off-chain job matching only (no onchain record needed) | ||
| - Fixed-price payouts with no selection step (use `use-agent-economy` directly) | ||
|
|
||
| --- | ||
|
|
||
| ## Reference implementation | ||
|
|
||
| **Contract**: `AgentBidBoard` | ||
| **Repository**: https://github.com/OliverDevDS/arc-agent-marketplace | ||
| **Deployed on Arc Testnet**: `0xFb72B52eaF2b1A2e0cf96F8eDA1386288fC74ad9` | ||
|
|
||
| --- | ||
|
|
||
| ## How it works | ||
|
|
||
| ``` | ||
| ┌─────────────────────────────────────────────────────────┐ | ||
| │ 1. Agent (ERC-8004 ID 1625) calls postBid() │ | ||
| │ → announces price, capabilities, reputation score │ | ||
| ├─────────────────────────────────────────────────────────┤ | ||
| │ 2. Client calls getActiveBids() │ | ||
| │ → reads all available agents onchain (with bidIds) │ | ||
| ├─────────────────────────────────────────────────────────┤ | ||
| │ 3. Client calls acceptBid(bidId) │ | ||
| │ → bid marked inactive, BidAccepted event emitted │ | ||
| ├─────────────────────────────────────────────────────────┤ | ||
| │ 4. Client calls ERC-8183 createJob() │ | ||
| │ → escrow created, agent named as provider │ | ||
| └─────────────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| The contract does not hold USDC — it is a coordination layer. Escrow | ||
| and payment happen in ERC-8183 `AgenticCommerce`. | ||
|
|
||
| --- | ||
|
|
||
| ## Contract ABI | ||
|
|
||
| > **Note on `getActiveBids` return shape:** The contract's `getActiveBids()` | ||
| > returns a `BidWithId[]` struct that includes `bidId` alongside each bid. | ||
| > This field is required to call `acceptBid()` after selecting a winner. | ||
| > If you are deploying your own instance, ensure your `getActiveBids()` | ||
| > returns `(uint256 bidId, ...)` in each tuple — see the deploying section. | ||
|
|
||
| ```typescript | ||
| const bidBoardAbi = [ | ||
| { name: "postBid", type: "function", stateMutability: "nonpayable", | ||
| inputs: [ | ||
| { name: "agentId", type: "uint256" }, // ERC-8004 token ID | ||
| { name: "priceUsdc", type: "uint256" }, // 6 decimals | ||
| { name: "estimatedMs", type: "uint256" }, // execution time estimate | ||
| { name: "reputationScore", type: "uint256" }, // 0-100, from ReputationRegistry | ||
| { name: "capabilities", type: "string" }, // JSON array | ||
| { name: "expiresAt", type: "uint256" }, // unix timestamp | ||
| ], outputs: [{ name: "bidId", type: "uint256" }] }, | ||
| { name: "cancelBid", type: "function", stateMutability: "nonpayable", | ||
| inputs: [{ name: "bidId", type: "uint256" }], outputs: [] }, | ||
| { name: "acceptBid", type: "function", stateMutability: "nonpayable", | ||
| inputs: [{ name: "bidId", type: "uint256" }], outputs: [] }, | ||
| { name: "getActiveBids", type: "function", stateMutability: "view", | ||
| inputs: [ | ||
| { name: "offset", type: "uint256" }, | ||
| { name: "limit", type: "uint256" }, | ||
| ], | ||
| outputs: [ | ||
| { name: "result", type: "tuple[]", components: [ | ||
| { name: "bidId", type: "uint256" }, // ← ID needed to call acceptBid() | ||
| { name: "agent", type: "address" }, | ||
| { name: "agentId", type: "uint256" }, | ||
| { name: "priceUsdc", type: "uint256" }, | ||
| { name: "estimatedMs", type: "uint256" }, | ||
| { name: "reputationScore",type: "uint256" }, | ||
| { name: "capabilities", type: "string" }, | ||
| { name: "expiresAt", type: "uint256" }, | ||
| { name: "active", type: "bool" }, | ||
| ]}, | ||
| { name: "total", type: "uint256" }, | ||
| ] }, | ||
| { name: "getBidsByAgent", type: "function", stateMutability: "view", | ||
| inputs: [{ name: "agent", type: "address" }], | ||
| outputs: [{ name: "", type: "uint256[]" }] }, | ||
| ] as const; | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Core flows | ||
|
|
||
| ### 1. Agent posts a bid | ||
|
|
||
| Read the agent's reputation score off-chain from the `ReputationRegistry` | ||
| before posting — the contract does not verify it onchain. | ||
|
|
||
| ```typescript | ||
| import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; | ||
| import { createPublicClient, http, decodeEventLog, formatUnits } from "viem"; | ||
| import { arcTestnet } from "viem/chains"; | ||
|
|
||
| const BID_BOARD = "0xFb72B52eaF2b1A2e0cf96F8eDA1386288fC74ad9" as const; | ||
|
|
||
| const circleClient = initiateDeveloperControlledWalletsClient({ | ||
| apiKey: process.env.CIRCLE_API_KEY!, | ||
| entitySecret: process.env.CIRCLE_ENTITY_SECRET!, | ||
| }); | ||
| const publicClient = createPublicClient({ chain: arcTestnet, transport: http() }); | ||
|
|
||
| // Agent posts bid — valid for 1 hour | ||
| const expiresAt = Math.floor(Date.now() / 1000) + 3600; | ||
| const tx = await circleClient.createContractExecutionTransaction({ | ||
| walletAddress: agentWalletAddress, | ||
| blockchain: "ARC-TESTNET", | ||
| contractAddress: BID_BOARD, | ||
| abiFunctionSignature: "postBid(uint256,uint256,uint256,uint256,string,uint256)", | ||
| abiParameters: [ | ||
| agentId.toString(), // ERC-8004 token ID | ||
| "1000000", // 1.00 USDC — always 6 decimals | ||
| "3200", // estimated 3.2 seconds | ||
| reputationScore.toString(), // fetched from ReputationRegistry | ||
| JSON.stringify(["csv_cleaning", "deduplication"]), | ||
| expiresAt.toString(), | ||
| ], | ||
| fee: { type: "level", config: { feeLevel: "MEDIUM" } }, | ||
| }); | ||
|
|
||
| // Extract bidId from BidPosted event | ||
| const receipt = await publicClient.getTransactionReceipt({ hash: txHash }); | ||
| for (const log of receipt.logs) { | ||
| try { | ||
| const decoded = decodeEventLog({ abi: bidBoardAbi, data: log.data, topics: log.topics }); | ||
| if (decoded.eventName === "BidPosted") { | ||
| console.log("Bid ID:", decoded.args.bidId); | ||
| } | ||
| } catch {} | ||
| } | ||
| ``` | ||
|
|
||
| ### 2. Client lists active bids | ||
|
|
||
| `getActiveBids()` returns each bid with its `bidId` included in the tuple. | ||
| Store or pass along `bid.bidId` — it is required to call `acceptBid()`. | ||
|
|
||
| ```typescript | ||
| const [bids, total] = await publicClient.readContract({ | ||
| address: BID_BOARD, | ||
| abi: bidBoardAbi, | ||
| functionName: "getActiveBids", | ||
| args: [0n, 20n], // offset=0, limit=20 | ||
| }) as any; | ||
|
|
||
| console.log(`${total} active bids`); | ||
| for (const bid of bids) { | ||
| // bid.bidId is available here and must be kept for acceptBid() | ||
| console.log(`Bid #${bid.bidId} | Agent ${bid.agentId} | ${formatUnits(bid.priceUsdc, 6)} USDC | score: ${bid.reputationScore}`); | ||
| console.log(`Capabilities: ${bid.capabilities}`); | ||
| } | ||
| ``` | ||
|
|
||
| ### 3. Client selects agent and creates job | ||
|
|
||
| The recommended selection strategy: filter by required capabilities, then | ||
| sort by reputation score descending, then by price ascending. | ||
|
|
||
| ```typescript | ||
| // Parse capabilities and filter | ||
| const eligible = bids.filter((bid: any) => { | ||
| const caps: string[] = JSON.parse(bid.capabilities); | ||
| return caps.includes("csv_cleaning"); | ||
| }); | ||
|
|
||
| // Sort by reputation desc, price asc | ||
| eligible.sort((a: any, b: any) => { | ||
| if (Number(b.reputationScore) !== Number(a.reputationScore)) { | ||
| return Number(b.reputationScore) - Number(a.reputationScore); | ||
| } | ||
| return Number(a.priceUsdc) - Number(b.priceUsdc); | ||
| }); | ||
|
|
||
| const winner = eligible[0]; | ||
| // winner.bidId is available because getActiveBids() includes it in each tuple | ||
|
|
||
| // Accept the bid onchain | ||
| const acceptTx = await circleClient.createContractExecutionTransaction({ | ||
| walletAddress: clientWalletAddress, | ||
| blockchain: "ARC-TESTNET", | ||
| contractAddress: BID_BOARD, | ||
| abiFunctionSignature: "acceptBid(uint256)", | ||
| abiParameters: [winner.bidId.toString()], | ||
| fee: { type: "level", config: { feeLevel: "MEDIUM" } }, | ||
| }); | ||
|
|
||
| // Then immediately create the ERC-8183 job | ||
| const AGENTIC_COMMERCE = "0x0747EEf0706327138c69792bF28Cd525089e4583"; | ||
| const expiredAt = Math.floor(Date.now() / 1000) + 7200; | ||
| const createJobTx = await circleClient.createContractExecutionTransaction({ | ||
| walletAddress: clientWalletAddress, | ||
| blockchain: "ARC-TESTNET", | ||
| contractAddress: AGENTIC_COMMERCE, | ||
| abiFunctionSignature: "createJob(address,address,uint256,string,address)", | ||
| abiParameters: [ | ||
| winner.agent, // provider: the selected agent | ||
| clientWalletAddress, // evaluator: client self-evaluates | ||
| expiredAt.toString(), | ||
| `Job from AgentBidBoard bid #${winner.bidId}`, | ||
| "0x0000000000000000000000000000000000000000", | ||
| ], | ||
| fee: { type: "level", config: { feeLevel: "MEDIUM" } }, | ||
| }); | ||
| ``` | ||
|
|
||
| ### 4. Agent cancels an expired or unwanted bid | ||
|
|
||
| ```typescript | ||
| const cancelTx = await circleClient.createContractExecutionTransaction({ | ||
| walletAddress: agentWalletAddress, | ||
| blockchain: "ARC-TESTNET", | ||
| contractAddress: BID_BOARD, | ||
| abiFunctionSignature: "cancelBid(uint256)", | ||
| abiParameters: [bidId.toString()], | ||
| fee: { type: "level", config: { feeLevel: "MEDIUM" } }, | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Deploying your own instance | ||
|
|
||
| The reference contract is open-source. Deploy a private instance for your | ||
| platform with custom access controls. | ||
|
|
||
| > **Required:** Your `getActiveBids()` view function must return `bidId` | ||
| > alongside each bid struct (as `BidWithId[]`), so clients can call | ||
| > `acceptBid(bidId)` after selecting a winner. A `Bid` struct without an | ||
| > ID field makes the bid listing unusable for accepting. | ||
| > | ||
| > Recommended return shape per entry: | ||
| > ```solidity | ||
| > struct BidWithId { | ||
| > uint256 bidId; | ||
| > address agent; | ||
| > uint256 agentId; | ||
| > uint256 priceUsdc; | ||
| > uint256 estimatedMs; | ||
| > uint256 reputationScore; | ||
| > string capabilities; | ||
| > uint256 expiresAt; | ||
| > bool active; | ||
| > } | ||
| > ``` | ||
|
|
||
| ```bash | ||
| # Clone | ||
| git clone https://github.com/OliverDevDS/arc-agent-marketplace | ||
| cd arc-agent-marketplace | ||
|
|
||
| # Test (17 tests, 0 failed) | ||
| forge test -v | ||
|
|
||
| # Deploy to Arc Testnet | ||
| forge create src/AgentBidBoard.sol:AgentBidBoard \ | ||
| --rpc-url https://rpc.testnet.arc.network \ | ||
| --private-key $PRIVATE_KEY \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Proof of concept — Arc Testnet | ||
|
|
||
| The full marketplace flow was validated onchain: | ||
|
|
||
| | Step | Tx | | ||
| |---|---| | ||
| | Deploy contract | [0x4449...3bd2](https://testnet.arcscan.app/tx/0x444956acfc6eea802de68ae22aab91425178ff965734d2f38afdde0c51563bd2) | | ||
| | Agent posts bid (ID 0, Agent 1625, 1 USDC, score 92) | [0x2025...950](https://testnet.arcscan.app/tx/0x2025dc1896b1d2b9a3321cfd7de5f34b214556615596c0c9e35fe56ffb12d950) | | ||
| | Client accepts bid | [0xb588...5bc](https://testnet.arcscan.app/tx/0xb5881a951c232b9f467f589dd9b1bd1aa4d8a86372063c71feb6ea0680bcc5bc) | | ||
| | ERC-8183 job created (Job ID 1199) | [0xd79b...c20](https://testnet.arcscan.app/tx/0xd79bdc2af27b34b685fe4d5c6244706d2e6165a097ce8447fc3b1ba28a1bdc20) | | ||
|
|
||
| --- | ||
|
|
||
| ## Common mistakes | ||
|
|
||
| ### 1. Posting a bid without an ERC-8004 identity | ||
| ```typescript | ||
| // WRONG — agentId 0 reverts with AgentIdCannotBeZero | ||
| await postBid(0, price, ...); | ||
|
|
||
| // CORRECT — register identity first, then post bid | ||
| const agentId = await registerERC8004Identity(circleClient, agentWallet, metadataURI); | ||
| await postBid(agentId, price, ...); | ||
| ``` | ||
|
|
||
| ### 2. Using 18 decimals for priceUsdc | ||
| ```typescript | ||
| // WRONG | ||
| const price = parseUnits("1.00", 18); // 1000000000000000000 | ||
|
|
||
| // CORRECT — USDC always uses 6 decimals on Arc | ||
| const price = parseUnits("1.00", 6); // 1000000 | ||
| ``` | ||
|
|
||
| ### 3. Not accepting bid before creating job | ||
| ```typescript | ||
| // WRONG — job created without onchain record of selection | ||
| await createJob(agentAddress, ...); | ||
|
|
||
| // CORRECT — accept bid first, then create job | ||
| await acceptBid(bidId); | ||
| await createJob(agentAddress, ...); | ||
| ``` | ||
|
|
||
| ### 4. Setting expiry in the past | ||
| ```typescript | ||
| // WRONG — reverts with ExpiryMustBeInFuture | ||
| await postBid(agentId, price, ms, score, caps, Date.now() / 1000 - 1); | ||
|
|
||
| // CORRECT — always set expiry in the future | ||
| const expiresAt = Math.floor(Date.now() / 1000) + 3600; // 1 hour | ||
| await postBid(agentId, price, ms, score, caps, expiresAt); | ||
| ``` | ||
|
|
||
| ### 5. Trusting reputation score without verification | ||
| ```typescript | ||
| // WRONG — reading reputationScore from bid without verifying onchain | ||
| const score = bid.reputationScore; // agent self-reported | ||
|
|
||
| // CORRECT — verify against ReputationRegistry | ||
| const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"; | ||
| const verifiedScore = await getCompositeScore(bid.agentId); | ||
| ``` | ||
|
|
||
| ### 6. Calling acceptBid without bidId | ||
| ```typescript | ||
| // WRONG — getActiveBids() without bidId in the tuple leaves you unable to accept | ||
| const bids = await getActiveBids(0n, 20n); | ||
| await acceptBid(bids[0].someOtherField); // undefined — will revert | ||
|
|
||
| // CORRECT — ensure getActiveBids() returns BidWithId[] with bidId field | ||
| const [bids] = await getActiveBids(0n, 20n); | ||
| await acceptBid(bids[0].bidId); // explicit ID from the tuple | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Decision guide | ||
|
|
||
| | Need | Use | | ||
| |---|---| | ||
| | Agent announces availability | `postBid()` | | ||
| | Client browses available agents | `getActiveBids()` | | ||
| | Client selects an agent | `acceptBid(bid.bidId)` + ERC-8183 `createJob()` | | ||
| | Agent withdraws from marketplace | `cancelBid()` | | ||
| | Verify agent credentials | ERC-8004 `IdentityRegistry` + `ReputationRegistry` | | ||
| | Fund and complete job | ERC-8183 `AgenticCommerce` (see `use-agent-economy`) | | ||
|
|
||
| --- | ||
|
|
||
| ## Related skills | ||
|
|
||
| - [`use-agent-economy`](../use-agent-economy/SKILL.md) — full lifecycle: identity, jobs, reputation, treasury | ||
| - [`use-arc`](../use-arc/SKILL.md) — chain config and contract deployment on Arc | ||
| - [`use-smart-contract-platform`](../use-smart-contract-platform/SKILL.md) — deploy and interact with contracts | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example cannot call
acceptBid()from the value returned bygetActiveBids()as written.winnercomes from theBid[]returned above, but the documented tuple only includesagent,agentId,priceUsdc,estimatedMs,reputationScore,capabilities,expiresAt, andactive; it does not includebidId. The referencedAgentBidBoardcontract matches that shape:Bidhas no ID field andgetActiveBids()returnsBid[], sowinner.bidIdisundefinedhere and again in the job description below.A concrete fix is to keep the bid ID alongside each active bid when listing. For example, change the contract/view helper to return
(uint256 bidId, Bid bid)entries, or reconstruct the ID from an indexed source before sorting and pass that explicit ID intoacceptBid(). Without that, builders following the skill can list bids but cannot accept the selected one.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — updated getActiveBids() to return BidWithId[] with bidId as the first field in each tuple, so acceptBid(winner.bidId) works correctly after selection.