Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions plugins/circle/skills/use-agent-marketplace/SKILL.md
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()],

Copy link
Copy Markdown

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 by getActiveBids() as written. winner comes from the Bid[] returned above, but the documented tuple only includes agent, agentId, priceUsdc, estimatedMs, reputationScore, capabilities, expiresAt, and active; it does not include bidId. The referenced AgentBidBoard contract matches that shape: Bid has no ID field and getActiveBids() returns Bid[], so winner.bidId is undefined here 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 into acceptBid(). Without that, builders following the skill can list bids but cannot accept the selected one.

Copy link
Copy Markdown
Author

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.

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