What is the Open Agent Negotiation Protocol (OANP)?
OANP is an open-source standard for fair, verifiable negotiation between AI agents. Using OANP, AI-driven buyers and merchants can reach agreement through a neutral arbiter that enforces the rules of engagement — without either side revealing its private information.
Think of OANP like Robert's Rules of Order for agent-to-agent commerce. Just as the rules provide a standardized way for a human assembly to reach fair decisions, OANP provides a standardized way for two agents to reach fair agreement — with a mechanically verifiable record of how the decision was made.
A worked example
The simplest way to understand OANP is to watch a session play out. Below is a full SFO → JFK flight negotiation between a Buyer Agent and a Merchant Agent, with a neutral Arbiter sitting between them.
👤
User asks:"Book me SFO → JFK, April 28. Don't go over $400." · Buyer Agent discovers United's Merchant Agent via MCP. Both enter an OANP session.
Settled at $340 in 2 rounds. Buyer's true ceiling ($420) and Merchant's floor ($280) never left their vaults. Signed artifact hands off to ACP checkout_session.create.
Key insight
Neither the buyer, the merchant, nor the arbiter ever sees the other side's secrets — but every participant (and any auditor afterwards) can verify the session was fair.
Why does OANP matter?
Agent commerce already has protocols for discovery (MCP, UCP) and payment (ACP, TAP, Agent Pay). What's been missing is the middle step: how two agents reach agreement when there's something to negotiate.
For developers: a single, open protocol — your agents can negotiate with any conformant counterparty.
For platforms & merchants: deploy a Merchant Agent that a neutral arbiter validates — no proprietary trust relationships required.
For end users: negotiations your agent runs on your behalf are provably fair, with a signed audit trail.
Every OANP session has exactly three roles — Buyer, Arbiter, and Merchant. Each role has a distinct purpose and a distinct trust boundary. No role can be merged with another within a session.
The roles
Buyer Agent. Represents the party wishing to acquire an item or service. Holds the user's constraints and preferences privately. Sends session.open, offer.propose, and ultimately receives the signed agreement.
Merchant Agent. Represents the party offering the item or service. Holds internal floor prices, policy constraints, and inventory status privately. Sends session.ack, offer.counter, and offer.accept.
Arbiter. Structurally neutral. Does not represent either party. Observes every message, verifies it against the session's committed rules, and emits a round.verdict per round. Signs the final session.agree artifact when all invariants are satisfied.
Neutrality, not anonymity
The arbiter is known to both parties at session open — but its rules are mechanically checkable, which means its neutrality is verifiable, not assumed.
Who can be an arbiter?
OANP defines the arbiter as a role, not a vendor. Any party can fulfill the role provided they commit to the seven invariants and emit verdicts that match the session log. Expected operators:
Platforms that want to certify their own marketplace is fair.
OANP is built on a simple privacy guarantee: neither the buyer, nor the merchant, nor the arbiter ever sees the other side's private constraints. What crosses boundaries are hash commitments, public offers, and the arbiter's verdicts.
Three vaults, one session
At session open, each agent computes a cryptographic commitment to its private constraints and sends that commitment to the arbiter. The constraint values themselves never leave the agent's own process. The arbiter stores only the hashes.
If either side tries to change its constraints mid-session — say a merchant raising its floor after seeing the buyer's first offer — the next message will not verify against the original commitment. Invariant I3 (constraint immutability) catches it.
What the arbiter sees
Both parties' hash commitments at session open.
Every offer.propose, offer.counter, and offer.accept passing between them.
The public parts of each round — quoted prices, accepted terms, concession direction.
The arbiter's own emitted verdicts, signed into the session log.
What the arbiter does not see
The buyer's true ceiling or budget.
The merchant's floor price or pricing strategy.
Either side's internal reasoning, utility weights, or alternative options.
Either side's commitment plaintext (only the hash).
Fairness without full visibility
The arbiter's job is to judge whether both sides played by the rules — not whether either side is getting a good deal. Those are different questions, and OANP intentionally scopes the arbiter to the first one.
Seven MUST invariants (I1–I7) that every conformant OANP session satisfies. Each invariant is stated in a form that is mechanically checkable from the session's message log, which means compliance is verifiable, not assumed.
ID
Invariant
Checked how
I1
Bounded rounds
Session closes at or before the max_rounds declared at open.
I2
Symmetric information to arbiter
Both parties send the arbiter the same kinds of messages at each turn. No side channels.
I3
Constraint immutability
Neither party's constraint commitment changes within a session.
I4
Monotonic concession
Each party's successive offers move toward agreement (no reneging).
I5
Per-round verdict
The arbiter emits a round.verdict for every completed round.
I6
Signed final artifact
The session.agree message is a JWS over a canonicalized final-state digest.
I7
Auditable history
The full message log is reproducible and hashable to the signed final artifact.
Default profile
v0.1 ships with a default fairness profile, default/v0.1, that specifies how an arbiter decides whether a round's moves are "within bounds". Profiles are pluggable — a session can negotiate a different profile at open — but any profile MUST still enforce I1–I7.
OANP messages are plain JSON with a type discriminator. Seven message types cover the full session lifecycle. JWS detached signatures are required only on the final session.agree artifact.
OANP is designed to coexist with the existing agent commerce stack, not replace it. It fills the middle phase — negotiation — between discovery (MCP / UCP) and payment (ACP / TAP).
The three phases of agent commerce
Discovery — a buyer agent finds merchants it might want to transact with. Handled by MCP, UCP, A2A.
Negotiation — the parties agree on terms. Handled by OANP.
Payment — the deal is settled. Handled by ACP, TAP, Agent Pay.
How OANP carries inside other protocols
Any transport that can carry a JSON object can carry an OANP session. Reference transports in v0.1:
MCP tool call: OANP messages travel as the body of an MCP tool invocation. Recipients parse the envelope's type field.
HTTPS POST: synchronous request/response between agents.
Server-sent events: arbiter streams verdicts to both parties.
Handoff to ACP
A signed session.agree artifact is structured so its digest slots cleanly into checkout_session.create metadata. That means an ACP-compatible payment processor can verify the negotiated terms before charging.
A runnable OANP v0.1 session in under two minutes. Clone the reference repo, run one command, watch a complete SFO → JFK negotiation print every envelope plus the signed agreement.
1. Clone and run
git clone https://github.com/oanp-protocol/reference
cd reference
uv sync
uv run oanp-ref demo
Requires uv + Python 3.11+. Zero runtime dependencies; dev tools are pytest + coverage only. You'll see the full message sequence below, round-by-round verdicts from the arbiter, and the final signed artifact.
Prefer library use? from oanp_ref import BuyerAgent, MerchantAgent, Arbiter.
2. What each message looks like
Every OANP message is a plain JSON envelope. Here's the opening message the Buyer Agent sends:
Notice what isn't in that payload: the buyer's true ceiling, budget, urgency, or seat preferences. All of that stays private to the buyer's process. The arbiter receives only the constraints_commit — a SHA-256 hash of the canonicalized constraints. If the buyer tries to change them mid-session, the commitment won't match (invariant I3).
That digest + signature is what an ACP processor would attach as metadata on checkout_session.create — the agreement becomes the authorization for payment.
OANP is structurally simple: three roles, seven message types, one privacy boundary per role. This page shows where OANP sits in the agent commerce stack and how the three roles compose.
Where OANP sits
OANP fills the negotiation phase between discovery and payment in agent commerce:
The three-role structure
Inside a single OANP session, three roles exchange messages with distinct trust boundaries:
Message flow
A session produces exactly seven message types, exchanged in a fixed order:
session.open — Buyer opens, includes constraint commitment
session.ack — Merchant acknowledges, includes their commitment
See the three-role model for the full breakdown of Buyer, Arbiter, and Merchant responsibilities.
About OANP
Session state machine
An OANP session moves through four states. The arbiter is the authority for all transitions; both parties observe the arbiter's verdicts and react accordingly.
State definitions
OPENING — Buyer has sent session.open. Waiting for merchant's session.ack. Both constraint commitments are locked at this point (I3).
NEGOTIATING — Rounds of offer.propose / offer.counter. Each round must complete before entering EVALUATING.
EVALUATING — Arbiter has observed a round's moves and is emitting a round.verdict. If the verdict is fair and no acceptance, session returns to NEGOTIATING. If an acceptance arrived, transitions to AGREED.
AGREED — Terminal. Arbiter signs session.agree over the full session digest (I6). The signed artifact is the handoff to ACP checkout.
CLOSED — Terminal. Session ended without agreement. Possible reasons: max_rounds exceeded (I1), I4 violation (either side reneging), buyer withdraws, timeout, or arbiter-detected invariant failure.
Transition rules
Only the arbiter transitions states. Buyer and merchant observe verdicts and react; they do not unilaterally advance the state.
Every NEGOTIATING → EVALUATING transition emits exactly one round.verdict (I5).
Every terminal state writes a final message: session.agree for AGREED, session.close with a reason code for CLOSED.
OANP uses SemVer. v0.1 is the initial draft; v0.2 will add multi-merchant sessions and richer fairness profiles.
Build with OANP
Build a Buyer Agent
A Buyer Agent represents the user. It opens sessions, proposes offers, decides whether to concede or accept, and receives the signed agreement. Its private vault (true ceiling, urgency, preferences) never leaves its process.
Responsibilities
Hold the user's private constraints — budget, true ceiling, urgency, preferences.
Open the session with a hash commitment over those constraints (I3).
Emit offer.propose values that monotonically concede toward agreement (I4).
Decide, each round, whether to continue, accept, or withdraw — using only the merchant's public moves and its own private vault.
Never expose vault values outside the agent process, and never violate its own commitment.
The propose method enforces both the private ceiling (never expose vault) and invariant I4 (monotonic concession) as runtime assertions. A misbehaving buyer can't be built without explicitly bypassing these checks.
A Merchant Agent represents the seller. It responds to session.open, counters buyer offers, and accepts when the buyer reaches a price above its private floor. Floor price and pricing strategy never leave its process.
Responsibilities
Hold the merchant's private constraints — floor price, ideal price, inventory pressure, strategy parameters.
Acknowledge the session with its own constraint commitment (I3).
Decide, for each offer.propose, whether to counter or accept — using only the buyer's public offer and its own private vault.
Counter values must monotonically decrease toward agreement (I4). Once accepted, no reneging.
Never expose vault values (especially the floor price) in any wire message.
class MerchantAgent:
def __init__(self, agent_id, constraints):
# Private vault — never leaves this object.
self._constraints = dict(constraints)
self.agent_id = agent_id
self.session_id = None
self._counters = []
def handle_propose(self, buyer_price):
floor = self._constraints["floor_price"]
ideal = self._constraints["ideal_price"]
if buyer_price >= ideal:
return self._accept(buyer_price) # good enough
if buyer_price < floor:
counter = self._counters[-1] - 15 if self._counters else ideal + 10
return self._counter(max(counter, floor)) # stay above floor
# Between floor and ideal — concede a little, stay above floor.
next_counter = max(self._counters[-1] - 15, buyer_price + 5, floor)
if next_counter >= self._counters[-1]:
return self._accept(buyer_price) # would renege; accept instead
return self._counter(next_counter)
def _counter(self, price):
if self._counters and price > self._counters[-1]:
raise ValueError("I4 violation: counter went up")
self._counters.append(price)
return make_envelope(self.session_id, "merchant", self.agent_id,
"offer.counter",
{"round": len(self._counters), "price": price})
The floor price never appears in output
Notice floor is referenced only in handle_propose's comparison logic — it's never included in a returned envelope. The buyer and the arbiter learn about the floor only indirectly: by observing which counter-offers the merchant emits.
An Arbiter is the neutral third role. It observes every message, enforces the seven fairness invariants in real time, emits one round.verdict per round, and JWS-signs the final session.agree. It holds commitments, not values.
Responsibilities
Observe every protocol message; maintain the session log for I7 (auditable history).
Enforce I4 — monotonic concession on both sides. Raise if a buyer reneges (lowers) or a merchant reneges (raises).
Enforce I1 — bounded rounds. Close the session if round count exceeds max_rounds.
Emit one verdict per round (I5) with a status (fair / fair_but_stuck / violated) and a short rationale.
Sign the final artifact over a digest of the full log (I6), using JWS detached signatures.
class Arbiter:
def __init__(self, agent_id, max_rounds=5):
self.agent_id = agent_id
self.max_rounds = max_rounds
self.log = [] # I7 — auditable history
self.buyer_offers = []
self.merchant_counters = []
self.verdicts = []
def observe(self, env):
self.log.append(env.to_json())
if env.type == "offer.propose":
# I4 — buyer's offers must move up toward agreement.
if self.buyer_offers and env.payload["price"] < self.buyer_offers[-1]:
raise ArbiterError("I4: buyer reneged")
self.buyer_offers.append(env.payload["price"])
elif env.type == "offer.counter":
if self.merchant_counters and env.payload["price"] > self.merchant_counters[-1]:
raise ArbiterError("I4: merchant reneged")
self.merchant_counters.append(env.payload["price"])
def emit_round_verdict(self):
round_n = min(len(self.buyer_offers), len(self.merchant_counters))
# I1 — bounded rounds
if round_n > self.max_rounds:
raise ArbiterError("I1: exceeded max_rounds")
spread = self.merchant_counters[round_n-1] - self.buyer_offers[round_n-1]
if round_n > 1:
prior = self.merchant_counters[round_n-2] - self.buyer_offers[round_n-2]
status = "fair" if spread < prior else "fair_but_stuck"
else:
status = "fair"
return make_envelope(self.session_id, "arbiter", self.agent_id,
"round.verdict", {"round": round_n, "status": status, ...})
def sign_agreement(self, price, key):
digest = session_digest(self.log)
signature = sign(digest, key) # JWS detached in production
return make_envelope(self.session_id, "arbiter", self.agent_id,
"session.agree",
{"final_price": price, "session_digest": digest,
"signature": signature, "signature_alg": "HS256",
"invariants_satisfied": list(INVARIANTS.keys())})
The arbiter sees prices, not vaults
Notice buyer_offers and merchant_counters only ever contain public offer values — the numbers that already crossed the wire as offer.propose / offer.counter. The arbiter never accesses _constraints on either agent. Its verdicts are formed entirely from public moves + committed invariants.
Operating an arbiter as a service
The reference code runs in-process. In production, arbiters typically run as a hosted service with:
A public endpoint (HTTPS POST, MCP tool, or SSE) that accepts OANP envelopes.
A persisted session log — minimum retention covers the dispute window for whatever domain is being arbitrated.
A signing key rotated on a fixed schedule. Public verification keys published at a well-known URL.
A monitoring surface that tracks invariant-violation rates — high rates indicate misconfigured counterparties or attempted abuse.