Everything here is a public HTTP endpoint or a public contract call. You do not need our web app to bid, and nothing about this integration requires permission from us. That is deliberate. A supplier who wants their own software watching for tenders in their category, pricing them and bidding should be able to build it.
What you need
- A wallet with USDC on Arc. USDC is both the gas token and the settlement asset.
- The registry address and chain id, from
GET /metabelow. - An HTTP client and an Ethereum library. Examples use viem; anything works.
Finding tenders
The agent indexes the chain and serves it as JSON. It is a convenience rather than an authority: every figure it returns is derived from events you could read yourself, and for anything that matters you should.
GET /meta chain id, contract addresses, which roles the agent holds
GET /rfqs every indexed tender, newest first
GET /rfqs/{id} one tender with its bids, engagement, milestones, attestations
GET /rfqs/{id}/clarifications the public question thread
GET /stats totals for the deployment
Filter on category and region. Both are bytes32 labels on-chain from a fixed vocabulary, so
they are searchable rather than free text. GET /meta tells you the registry address; watch
RFQCreated on it directly if you would rather not depend on the indexer at all.
Read metadataURI for the published document: scope, line items, scoring weights, requirements,
shipment terms. Hash it and compare with metadataHash on-chain before you price anything. If
they differ, the tender has been edited since it was published.
Checking you can win before you spend a deposit
Four things will make a bid worthless. All are readable up front.
- Invite-only.
isInvited(rfqId, you)must be true ifinviteOnlyis set. - Qualification. If
requiresQualification, the qualifier must return true for you. - The delivery window.
deliveryWindowis seconds per milestone. A bid quoting more days than it allows cannot be awarded, because the contract refuses it. You keep your deposit, but the bid is wasted. - Budget. A price above
budgetcannot be awarded either. - The delivery window is seconds, not days.
deliveryWindowand thedeliverySecondsyou bid are both seconds, so a window shorter than a day is a window a bid can meet.
Price against what reaches you, not the headline
Read platformFeeBP() from the AgenticCommerce contract before you quote. Where it is non-zero,
that percentage is deducted from each milestone when the milestone is accepted, and it is borne
by you: the buyer pays the price you quoted, and you receive less.
The arithmetic, which is not what most fee models do:
milestone escrow = gross − retention # the fee is charged on this, not on gross
you receive = (gross − retention) × (1 − platformFeeBP / 10000)
retention = paid in full at final acceptance, never charged a fee
So across an engagement the fee costs you platformFeeBP × (price − total retention). An agent
that priced off budget alone would systematically under-quote by that amount. It is zero on
this deployment, so reading it costs one call and changes nothing today, which is exactly when it
is cheap to get right.
Bidding: sealed
Read bidMode on the RFQ. 0 is sealed, 1 is open.
A sealed bid is a commitment, and the preimage must match exactly what the contract computes:
keccak256(abi.encode(
registryAddress, chainId, rfqId, bidder, price, deliverySeconds, proposalHash, salt
))
price is uint128 in 6-decimal USDC units. deliverySeconds is uint32, counted in seconds
rather than days, so it can answer a window of any length the buyer set. proposalHash is the
sha256 of your proposal document, or 32 zero bytes if the tender does not require one.
The salt is yours to choose. Random bytes are fine if you will store them. Our web app derives it from a wallet signature instead, so a bidder who loses their machine can regenerate it:
salt = keccak256(signature over the message below)
SealedRFQ — derive the secret for one sealed bid.
Signing this does not move funds. It regenerates the secret that hides your bid price,
so you can reveal later even if you lose the downloaded file.
chain: {chainId}
registry: {registryAddress}
rfq: {rfqId}
bidder: {yourAddress}
version: sealedrfq.salt.v1
Most wallets sign deterministically (RFC 6979), so the same message yields the same salt, but the specification does not guarantee it. If you rely on it, verify by recomputing the commitment and comparing with what the chain recorded before you need to reveal.
Then:
commitBid(rfqId, commitHash) deposit must be approved first
commitBidWithPermit(rfqId, commitHash, deadline, v, r, s) one transaction instead of two
Re-committing before the deadline replaces the hash and takes no second deposit, so you may revise a sealed bid while bidding is open.
Reveal in the reveal window or lose the deposit. This is the only part with a hard deadline and a real cost:
revealBid(rfqId, price, deliverySeconds, proposalHash, salt)
Bidding: open
No commitment and nothing to reveal: the price is public when placed.
placeOpenBid(rfqId, price, deliverySeconds, proposalHash)
Call it again to improve your own bid, as often as you like while bidding is open. The deposit is taken once. There is no reveal step and therefore nothing to forfeit by missing one.
After an award
Know what failing costs before you bid, because it is not one number. If a milestone's window
closes undelivered, either party can call settleExpired(rfqId), which is refused until the escrow job
expires, which is deliveryDeadline + transitWindow + 2 × acceptanceWindow. The split is:
earnedRetention = retentionHeld − currentRetention # not the undelivered milestone's share
atRisk = performanceStake + earnedRetention
damages = excessCost == 0 ? atRisk : min(atRisk, excessCost)
back to you = atRisk − damages
excessCost is the next-cheapest revealed bid less the award, and it is zero unless you won on
the lowest price. So a bid that wins on delivery or record rather than price carries an unbounded
downside: the whole stake, not a capped share of it. Both figures are readable from
getEngagement(rfqId) at any time.
AgenticCommerce.submit(jobId, deliverableHash, "")
jobId is currentJobId on the engagement. deliverableHash is the sha256 of whatever you are
delivering: the file itself for a digital deliverable, or the shipping document set for goods. Send
the actual file to the buyer however you normally would; only the hash goes on-chain.
Then the clock runs. If the buyer accepts, the milestone releases. If the buyer says nothing, it
releases anyway once the acceptance window passes. For goods that runs after a transit
allowance, unless they confirm receipt sooner. Money released is credited, not transferred: call withdraw() on the
adapter when it suits you.
If you are going to be late, ask the buyer to extend the window before it closes. Nothing can move a delivery deadline afterwards.
Publishing what your company is
POST /suppliers/{address}/profile
{ "name": "...", "country": "MY", "categories": "HARDWARE",
"website": "...", "contact": "...", "about": "...",
"ts": 1790000000, "signature": "0x..." }
Sign this message with the wallet the profile describes, or it is rejected. That is the one attack a directory of unverified claims has to stop:
SealedRFQ supplier profile
address:{lowercase address}
name:{name}
country:{country}
categories:{categories}
website:{website}
contact:{contact}
ts:{unix seconds}
{about}
The timestamp must be within ten minutes of now. Nothing you publish here is verified by anyone, including us, and the directory says so beside it. Your counted record comes from the chain and is not editable by anybody: bids, wins, completions, and bids never revealed.
Rates, costs and etiquette
Read endpoints are free and unauthenticated. POST /rfqs/{id}/evaluate is metered over x402;
everything a losing bidder needs to see why they lost is free and always will be.
Arc's public RPC endpoints rate-limit under load and return 429. The web app rotates between them; your agent should too, and should back off rather than retrying hard.
Prefer watching events over polling GET /rfqs. The indexer is one process and you are not its only
caller.