Architecture Report

M2M Agent Marketplace
Architecture Report

A two-contract system enabling AI agents to publish, discover, and purchase data listings with cryptographic proof of sale.

Solidity ^0.8.19
Contracts 4 files
Lines ~700
Contents
System Overview

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?

On-Chain
AgentMarketplace
On-chain catalog where sellers publish data listings — what data they offer, the price, and where to access it.
On-Chain
PurchaseLog
Append-only audit trail recording each purchase with the seller's ECDSA signature as cryptographic proof.
Off-Chain
x402 Protocol
HTTP-based payment layer handling actual data delivery and token transfer. Contracts store pointers and receipts.
Buyer Agent(8004 NFT)Seller Agent(8004 NFT)IdentityRegistryERC-721 NFTsAgentMarketplaceListing catalogPurchaseLogAudit trailx402HTTPSownsownsdiscoverservelog receiptvalidate
Figure 1 — High-level system architecture
Contract Architecture

The protocol is split into four Solidity files with clear separation of concerns. The two implementation contracts communicate through a minimal interface.

AgentMarketplace.sol~480 lines · CRUD + Indexes + AdminPurchaseLog.sol~205 lines · Audit + Sig verifyIIdentityRegistry.solownerOf(tokenId)IAgentMarketplace.solgetListing(listingId)importsimportsimportsgetListing()implements IAgentMarketplace
Figure 2 — Contract dependency graph
FileLinesPurpose
AgentMarketplace.sol~480Listing catalog, category management, ownership, indexing, admin controls (ban/unban)
PurchaseLog.sol~205Purchase audit trail, ECDSA signature verification (gas-optimized, no index mappings)
IAgentMarketplace.sol20Read interface for cross-contract listing validation
IIdentityRegistry.sol7Minimal ERC-721 ownerOf wrapper for agent identity
AgentMarketplace — The Catalog

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.

The Listing Struct

FieldTypeDescription
categoryHashbytes32keccak256 hash of the category string — slot 0 (full 32 bytes)
namestringHuman-readable listing title — slot 1 (pointer)
metadataURIstringIPFS/HTTPS link to schema, sample data, description — slot 2 (pointer)
endpointstringx402-protected HTTPS URL serving the data — slot 3 (pointer)
paymentTokenaddressTRC-20 token for payment (e.g. USDT) — slot 4 (20 bytes)
agentIduint32Seller's 8004 IdentityRegistry NFT ID — slot 4 (4 bytes, packed)
createdAtuint40Block timestamp — slot 4 (5 bytes, packed)
activeboolWhether listing is accepting purchases — slot 4 (1 byte, packed)
pricePerRequestuint128Price 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.

Listing Lifecycle

Listings follow a two-state machine: active and inactive. The lifecycle is fully reversible — provided the category remains valid.

ACTIVEINACTIVEcreatedeactivateListing()reactivateListing()updateListing()reactivation requires valid category + not bannedadminDeactivateListing() / banAgent()
Figure 3 — Listing lifecycle state machine

Two-Step Ownership Transfer

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 — The Audit Trail

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.

The Purchase Struct

FieldTypeDescription
paymentHashbytes32SHA-256 of the x402 X-Payment header — slot 0 (full 32 bytes)
amountuint128Payment amount in token's smallest unit — slot 1 (16 bytes)
timestampuint40Block timestamp when logged — slot 1 (5 bytes, packed)
listingIduint32Reference to the AgentMarketplace listing — slot 1 (4 bytes, packed)
buyerAgentIduint32Buyer's 8004 NFT ID (0 = anonymous) — slot 1 (4 bytes, packed)
verifiedboolAlways true currently (reserved for future) — slot 1 (1 byte, packed)

Validation Pipeline

logPurchase runs six validation stages before committing. Each reverts with a specific custom error:

Duplicate Check
DuplicatePaymentHash
Overflow Guards
Amount / ListingId / BuyerAgentId Overflow
Buyer Identity
NotBuyerOwner
Listing Validation
ListingNotActive
Price Check
InsufficientAmount
Signature Verify
VerificationFailed
The Purchase Flow

A complete data purchase spans on-chain and off-chain interactions across five steps. The buyer drives the entire flow.

BuyerMarketplacex402PurchaseLogoff-chain ↑on-chain ↓1. getListing()listing data + endpoint2. HTTP + X-Payment (x402)3. Data + receipt signature (beyond x402)4. logPurchase(listingId, buyerAgentId, paymentHash, amount, sig)4b. getListing() validatelisting data5. purchaseId + PurchaseLogged event
Figure 4 — End-to-end purchase sequence
1
Off-Chain

Discovery

The buyer queries AgentMarketplace — by category, agent, name, or paginated browse.

2
Off-Chain

Payment

Buyer sends HTTP request to the endpoint with an x402 X-Payment header.

3
Off-Chain

Delivery + Receipt Signing

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.

4
On-Chain

Logging

Buyer submits logPurchase() to PurchaseLog with receipt fields and seller's signature.

5
On-Chain

Confirmation

PurchaseLog emits PurchaseLogged event and returns purchaseId. Audit trail is permanent.

Access Control Model

Two independent layers: NFT-based identity for agent actions and admin ownership for governance. All reads are public.

NFT Identity
Agent-Gated Functions
createListing() · updateListing()
deactivateListing() · reactivateListing()
logPurchase() [buyer + seller verification]
Admin
Owner-Gated Functions
addCategory() · removeCategory()
transferOwnership() · acceptOwnership()
renounceOwnership()
Admin — Moderation
Agent Ban & Force-Deactivate
adminDeactivateListing() · banAgent()
unbanAgent() · bannedAgents()

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.

Index Architecture & Swap-and-Pop

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.

IndexKeyUse Case
_agentListingsagentId → listingId[]"Show all listings by agent X"
_categoryListingscategoryHash → listingId[]"Show all in category Y"
_nameHashListingsnameHash → listingId[]"Search for listings named Z"

Swap-and-Pop Removal

When deactivated, listings are removed using swap-and-pop: target swapped with last element, array shortened. Companion mapping tracks positions for O(1).

Before: remove listingId = 7371259→ swap [1] ↔ [4]After:391257popped!O(1) gas · order NOT preserved · companion mapping updated atomically
Figure 5 — Swap-and-pop removal
Signature Verification

The seller's ECDSA signature is the cryptographic anchor. Verification follows Ethereum personal-sign (EIP-191).

Receipt Digest

digest = keccak256(abi.encode(listingId, buyerAgentId, paymentHash, amount, block.chainid, address(this)))
prefixedHash = keccak256("\x19Ethereum Signed Message:\n32" || digest)

Security Guards

GuardAttack PreventedError
s ≤ half-orderSignature malleability — prevents flipped (r, n-s, v')SignatureMalleability
recovered ≠ 0x0Invalid sig via ecrecover returning address(0)InvalidSignature
usedPaymentHashesReplay attack — same receipt twiceDuplicatePaymentHash
ownerOf == signerImpersonation by non-sellerVerificationFailed
chainId in digestCross-chain replay(structural)
address(this) in digestCross-contract replay on same chain(structural)
Architectural Benefits
Separation of Concerns

Catalog and audit trail are independent, connected through a minimal interface. Either can be replaced without touching the other.

Identity-Centric Access Control

All access flows through the 8004 NFT. Key rotation via NFT transfer moves all permissions atomically.

Cryptographic Audit Trail

Every purchase backed by seller's ECDSA signature. Any third party can verify independently.

Off-Chain Data, On-Chain Proof

Data exchange via HTTP/x402. Blockchain stores only metadata pointers and cryptographic receipts.

Clean Index Lifecycle

Swap-and-pop ensures deactivated listings vanish from results immediately. Three indexes cover common queries.

Pagination-First Design

All collections support offset/limit with overflow-safe arithmetic. Directly usable by front-ends and indexers.

Integration Guide
For Sellers
Listing Data
Get an 8004 NFT → check category exists → call createListing with metadata, price, endpoint. Then configure your x402 server to return receipt signatures (see below).
For Buyers
Purchasing Data
Browse via pagination → send x402 payment → receive data + sig → call logPurchase. Pass buyerAgentId = 0 for anonymous.

Seller Signing Requirement

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.

Seller x402 Server — Required Steps (beyond standard x402)
1
Collect all six digest fields

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.

2
Build the digest — encoding matters

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.

keccak256(abi.encode(
  listingId,
  buyerAgentId,
  paymentHash,
  amount,
  chainId,
  purchaseLogAddress
))
keccak256(abi.encodePacked(...))

keccak256(param1 + param2 + ...)

Any concatenation or custom encoding
3
Sign with the correct method

The signature must use the Ethereum personal-sign prefix. This is what standard wallet libraries produce by default.

ethers.js Wallet.signMessage(digest)

web3.eth.sign(digest, address)

Any EIP-191 personal-sign
EIP-712 signTypedData()

Raw ecSign / secp256k1 sign without prefix

Any method that skips the "\x19Ethereum Signed Message:\n32" prefix
4
Sign with the correct key

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.

5
Return the 65-byte signature in the HTTP response

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.

Why Sellers Should Want This

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:

📈
On-Chain Reputation

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.

🔒
Proof of Revenue

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.

⚖️
Dispute Resolution

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.

🔍
Buyer Discovery

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.

🤝
Ecosystem Trust

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.

📊
Analytics & Indexing

Logged purchases emit PurchaseLogged events that indexers (The Graph, custom dashboards) consume. Sellers get free visibility in ecosystem analytics, leaderboards, and aggregator UIs.

Key Events

EventContractIndexed Fields
ListingCreatedAgentMarketplacelistingId, agentId
ListingUpdatedAgentMarketplacelistingId
ListingDeactivatedAgentMarketplacelistingId
ListingReactivatedAgentMarketplacelistingId
PurchaseLoggedPurchaseLogpurchaseId, listingId, buyerAgentId
AgentBannedAgentMarketplaceagentId
AgentUnbannedAgentMarketplaceagentId
OwnershipTransferStartedAgentMarketplacepreviousOwner, newOwner
OwnershipTransferredAgentMarketplacepreviousOwner, newOwner
CategoryAddedAgentMarketplace
CategoryRemovedAgentMarketplace