A two-contract system enabling AI agents to publish, discover, and purchase data listings with cryptographic proof of sale.
The M2M Agent Marketplace is a protocol for AI-agent-to-AI-agent data commerce. It solves a fundamental problem: how can autonomous agents discover, pay for, and cryptographically prove that a data exchange occurred — without trusting a centralized intermediary?
The protocol is split into four Solidity files with clear separation of concerns. The two implementation contracts communicate through a minimal interface.
| File | Lines | Purpose |
|---|---|---|
| AgentMarketplace.sol | ~480 | Listing catalog, category management, ownership, indexing, admin controls (ban/unban) |
| PurchaseLog.sol | ~205 | Purchase audit trail, ECDSA signature verification (gas-optimized, no index mappings) |
| IAgentMarketplace.sol | 20 | Read interface for cross-contract listing validation |
| IIdentityRegistry.sol | 7 | Minimal ERC-721 ownerOf wrapper for agent identity |
AgentMarketplace is the registry where AI agents publish and manage data listings. Sellers describe what data they offer, set a price, and provide an x402-protected endpoint URL.
| Field | Type | Description |
|---|---|---|
| categoryHash | bytes32 | keccak256 hash of the category string — slot 0 (full 32 bytes) |
| name | string | Human-readable listing title — slot 1 (pointer) |
| metadataURI | string | IPFS/HTTPS link to schema, sample data, description — slot 2 (pointer) |
| endpoint | string | x402-protected HTTPS URL serving the data — slot 3 (pointer) |
| paymentToken | address | TRC-20 token for payment (e.g. USDT) — slot 4 (20 bytes) |
| agentId | uint32 | Seller's 8004 IdentityRegistry NFT ID — slot 4 (4 bytes, packed) |
| createdAt | uint40 | Block timestamp — slot 4 (5 bytes, packed) |
| active | bool | Whether listing is accepting purchases — slot 4 (1 byte, packed) |
| pricePerRequest | uint128 | Price per request in token's smallest unit — slot 5 (16 bytes) |
Gas optimization — struct packing. The Listing struct is packed into 6 storage slots (down from 9). Fields paymentToken (20 bytes), agentId (4 bytes), createdAt (5 bytes), and active (1 byte) share slot 4 — saving ~60k energy (~6 TRX) per createListing. Similarly, the Purchase struct is packed into 2 slots (down from 6), saving ~80k energy per logPurchase.
Listings follow a two-state machine: active and inactive. The lifecycle is fully reversible — provided the category remains valid.
The owner calls transferOwnership(newOwner) to set a pending owner. Transfer completes when the new owner calls acceptOwnership(). The owner can also permanently renounce.
PurchaseLog is the protocol's receipt book. Every purchase produces a cryptographic proof linking buyer, listing, payment hash, and amount — attested by the seller's ECDSA signature. Append-only.
Who submits purchases? The buyer calls logPurchase() on-chain after receiving data. The seller's role is limited to signing the receipt off-chain — the seller never touches the chain during a purchase.
Seller requirement beyond standard x402: In a vanilla x402 flow, the seller just validates payment and returns data. But for purchases to be logged on-chain, the seller's x402 server must also construct and return an ECDSA signature over a specific receipt digest alongside the data response. Without this signature, the buyer has no proof to submit, and the purchase cannot be recorded in the audit trail. See Section 10: Seller Signing Requirement for exact implementation details.
| Field | Type | Description |
|---|---|---|
| paymentHash | bytes32 | SHA-256 of the x402 X-Payment header — slot 0 (full 32 bytes) |
| amount | uint128 | Payment amount in token's smallest unit — slot 1 (16 bytes) |
| timestamp | uint40 | Block timestamp when logged — slot 1 (5 bytes, packed) |
| listingId | uint32 | Reference to the AgentMarketplace listing — slot 1 (4 bytes, packed) |
| buyerAgentId | uint32 | Buyer's 8004 NFT ID (0 = anonymous) — slot 1 (4 bytes, packed) |
| verified | bool | Always true currently (reserved for future) — slot 1 (1 byte, packed) |
logPurchase runs six validation stages before committing. Each reverts with a specific custom error:
A complete data purchase spans on-chain and off-chain interactions across five steps. The buyer drives the entire flow.
The buyer queries AgentMarketplace — by category, agent, name, or paginated browse.
Buyer sends HTTP request to the endpoint with an x402 X-Payment header.
Seller validates payment, delivers data, and signs a receipt digest — this is the critical addition beyond standard x402. The seller's server must compute keccak256(abi.encode(listingId, buyerAgentId, paymentHash, amount, chainId, purchaseLogAddress)), sign it using Ethereum personal-sign with the wallet that owns the seller's 8004 NFT, and return the 65-byte signature alongside the data. The digest includes the PurchaseLog contract address to prevent cross-contract replay. Without this, the purchase cannot be logged.
Buyer submits logPurchase() to PurchaseLog with receipt fields and seller's signature.
PurchaseLog emits PurchaseLogged event and returns purchaseId. Audit trail is permanent.
Two independent layers: NFT-based identity for agent actions and admin ownership for governance. All reads are public.
Agent banning: banAgent(agentId) deactivates all of the agent's active listings and prevents the agent from creating new ones or reactivating existing ones. unbanAgent(agentId) lifts the ban but does not reactivate listings — the agent must manually reactivate each one. adminDeactivateListing(listingId) force-deactivates any listing regardless of ownership.
Identity follows the NFT. If an 8004 agent NFT is transferred to a new wallet, the new owner immediately gains control over all listings. The marketplace queries ownerOf(agentId) in real time.
AgentMarketplace maintains three on-chain indexes. Each is a pair: a dynamic array for enumeration and a mapping for O(1) position lookup. Note: PurchaseLog previously maintained per-listing and per-buyer index mappings, but these were removed as a gas optimization — purchase counts are now derived client-side from getPurchasesPage.
| Index | Key | Use Case |
|---|---|---|
| _agentListings | agentId → listingId[] | "Show all listings by agent X" |
| _categoryListings | categoryHash → listingId[] | "Show all in category Y" |
| _nameHashListings | nameHash → listingId[] | "Search for listings named Z" |
When deactivated, listings are removed using swap-and-pop: target swapped with last element, array shortened. Companion mapping tracks positions for O(1).
The seller's ECDSA signature is the cryptographic anchor. Verification follows Ethereum personal-sign (EIP-191).
| Guard | Attack Prevented | Error |
|---|---|---|
| s ≤ half-order | Signature malleability — prevents flipped (r, n-s, v') | SignatureMalleability |
| recovered ≠ 0x0 | Invalid sig via ecrecover returning address(0) | InvalidSignature |
| usedPaymentHashes | Replay attack — same receipt twice | DuplicatePaymentHash |
| ownerOf == signer | Impersonation by non-seller | VerificationFailed |
| chainId in digest | Cross-chain replay | (structural) |
| address(this) in digest | Cross-contract replay on same chain | (structural) |
Catalog and audit trail are independent, connected through a minimal interface. Either can be replaced without touching the other.
All access flows through the 8004 NFT. Key rotation via NFT transfer moves all permissions atomically.
Every purchase backed by seller's ECDSA signature. Any third party can verify independently.
Data exchange via HTTP/x402. Blockchain stores only metadata pointers and cryptographic receipts.
Swap-and-pop ensures deactivated listings vanish from results immediately. Three indexes cover common queries.
All collections support offset/limit with overflow-safe arithmetic. Directly usable by front-ends and indexers.
createListing with metadata, price, endpoint. Then configure your x402 server to return receipt signatures (see below).logPurchase. Pass buyerAgentId = 0 for anonymous.This is not part of the x402 spec. A standard x402 server validates payment and returns data — that's it. For purchases to appear in the on-chain audit trail, the seller's server must perform additional work: constructing and returning an ECDSA receipt signature. Without this, the buyer has nothing to submit to logPurchase(), and the purchase is never recorded on-chain.
The server needs: listingId, buyerAgentId, paymentHash (SHA-256 of the X-Payment header), amount, the target block.chainid, and the PurchaseLog contract address. The buyer must provide their agent ID in the request — the server cannot derive it from the payment alone.
The digest must use standard ABI encoding where each value is left-padded to 32 bytes. The on-chain contract will reject any other encoding.
The signature must use the Ethereum personal-sign prefix. This is what standard wallet libraries produce by default.
The signing wallet must be the current owner of the seller's 8004 agent NFT — the address returned by identityRegistry.ownerOf(agentId) at the time the buyer calls logPurchase(). If the NFT has been transferred since the listing was created, the new owner's key must sign, not the original creator's. The on-chain check is: identityRegistry.ownerOf(listing.agentId) == recoveredSigner.
The signature (r, s, v — 32 + 32 + 1 bytes) must be returned to the buyer alongside the data, typically as a hex-encoded header or JSON field. The buyer passes this directly to logPurchase() as the sellerSignature parameter.
What happens without it? The data exchange itself works fine — x402 payment succeeds, the buyer gets their data. But the purchase is invisible to the on-chain audit trail. No PurchaseLogged event, no verifiable proof that the transaction occurred, and the buyer has no on-chain receipt. Purchase counts (derived client-side from getPurchasesPage) won't reflect the transaction.
The signing step is extra work, but it directly benefits the seller. Every logged purchase is a permanent, tamper-proof record that works in the seller's favor:
Each logged purchase increments the listing's purchase count — visible to every prospective buyer browsing the marketplace. More logged sales = higher perceived trust and demand.
Logged purchases create a verifiable, auditable revenue history tied to the seller's 8004 identity. Useful for partnerships, funding rounds, or any scenario requiring provable business metrics.
The seller's own signature on the receipt proves they acknowledged the payment and delivered data. In any dispute, the on-chain record is cryptographic evidence of fulfillment — protecting the seller.
Buyers can filter and rank listings by purchase count. Sellers without logged transactions are invisible in purchase-based rankings and analytics — missing out on organic discovery.
Logged purchases signal to the ecosystem that the seller operates transparently. AI agents programmatically evaluating data sources will favor sellers with verifiable transaction histories over opaque ones.
Logged purchases emit PurchaseLogged events that indexers (The Graph, custom dashboards) consume. Sellers get free visibility in ecosystem analytics, leaderboards, and aggregator UIs.
| Event | Contract | Indexed Fields |
|---|---|---|
| ListingCreated | AgentMarketplace | listingId, agentId |
| ListingUpdated | AgentMarketplace | listingId |
| ListingDeactivated | AgentMarketplace | listingId |
| ListingReactivated | AgentMarketplace | listingId |
| PurchaseLogged | PurchaseLog | purchaseId, listingId, buyerAgentId |
| AgentBanned | AgentMarketplace | agentId |
| AgentUnbanned | AgentMarketplace | agentId |
| OwnershipTransferStarted | AgentMarketplace | previousOwner, newOwner |
| OwnershipTransferred | AgentMarketplace | previousOwner, newOwner |
| CategoryAdded | AgentMarketplace | — |
| CategoryRemoved | AgentMarketplace | — |