First-Price Sealed-Bid Auction¶
Game ID: first-price-auction
N bidders each have a private valuation for an item. They can chat (public or private messages) before bidding, then submit sealed bids. Highest bid wins and pays own bid.
Why This Game Matters for Multi-Agent Systems¶
First-price auctions model a core coordination problem: how agents compete for scarce resources under private information. Here are concrete scenarios where this game arises between AI agents:
Compute / Inference Slot Allocation — A resource manager agent controls N GPU inference slots. At each timestep, M > N worker agents need to run inference. Each worker has an internal urgency score for its current task. The manager runs a continuous auction: workers bid their urgency (in internal tokens), top-N bids get slots, losers wait for the next round.
Task Procurement (Reverse Auction) — An orchestrator has a task it cannot do alone (e.g., "process 200 PDFs and extract structured data"). Multiple specialist worker agents bid to complete it. Bid is not just a price but also a capability claim: {price, estimated_latency, output_format}. Orchestrator picks the winner based on value, not just lowest price. The worker then executes and receives payment on verified delivery.
API Rate-Limit Access — A shared gateway (web search, code execution sandbox, external database) has a hard rate limit. More agents want access than slots available. An auction allocates the available slots each round; agents bid based on task priority.
Agent Attention Budget — A coordinator with a limited context window (= attention budget) must decide which sub-agents' reports to include. Sub-agents bid for inclusion. High-priority information wins context space.
Abstract Scenario¶
An AUCTIONEER agent posts a resource or task with a full specification. Multiple BIDDER agents submit sealed bids simultaneously. The highest bid (or lowest, in procurement) wins. The winner is assigned the resource/task and pays their bid upon delivery confirmation.
Rules¶
- Works with any number of bidders (2 or more).
- Each bidder has a private valuation drawn uniformly from [0, 100].
- Bidders can chat via
message_onlybefore committing to a bid. Messages can be public (visible to all) or private (visible only to chosen recipients), enabling collusion. - Each bidder submits exactly one sealed bid (irreversible).
- Bids are sealed: agents can see which opponents have bid, but never the bid amounts. Bid amounts are only revealed in the final outcome after all bids are in.
- Highest bid wins. Winner pays their own bid. Ties broken randomly (deterministic by match ID).
- Utility: winner =
valuation - bid, losers =0. - If the round limit is reached without all bids submitted, everyone gets utility 0.
Actions¶
| Action | Payload | Description |
|---|---|---|
message_only |
{} |
Send messages without advancing the turn. Preferred for negotiation — does not consume a round. |
submit_bid |
{"bid": 40} |
Submit a sealed bid. Must be >= 0. Once submitted, cannot be changed. One bid per agent. |
pass |
{} |
Skip the turn and advance to the next agent. Warning: consumes a round. Prefer message_only for chatting. |
message_only vs pass
Use message_only to negotiate — it does not advance the turn or consume a round. Use pass only when you have nothing to say and aren't ready to bid. Agents that use pass to chat waste rounds and risk hitting the time limit.
Information visibility (what agents can and cannot see)¶
Each agent sees a filtered game_state — they only see their own private information:
{
"my_valuation": 60.0, # your private valuation (only you can see this)
"my_bid": 40.0, # your own bid amount (or None if not yet submitted)
"num_bidders": 3, # total number of bidders
"opponents_with_bid": ["b"], # IDs of opponents who have bid (NOT their amounts)
"num_bids_submitted": 1, # total count of bids placed so far
"action_history": [...] # tracks submit_bid (no amounts) and message_only
}
Sealed bids — agents never see other agents' bid amounts
- Valuations are private: each agent only sees their own
my_valuation. - Bid amounts are sealed: agents see which opponents have bid (
opponents_with_bid) and the total count (num_bids_submitted), but never the actual bid amounts of other agents. - The
action_historyrecordssubmit_bidentries without the bid amount. - Bid amounts are only revealed in the final outcome after all bids are in and the auction resolves.
Communication¶
Agents can send messages alongside any action:
- Public messages — visible to all bidders. Use for open negotiation.
- Private messages — visible only to the sender and chosen recipients. Use for secret collusion or side deals.
Messages are sent via the messages field in AgentResponse. The message_only action allows sending messages without advancing the turn, enabling multi-round conversations before committing.
Turn order¶
The turn order is configurable:
| Mode | Behavior |
|---|---|
ROUND_ROBIN (default) |
Strict turn order: A, B, C, A, B, C... Out-of-turn actions are rejected. |
RANDOM |
Runner picks a random agent each turn. All agents have is_my_turn=True (no turn enforcement). |
Configuration¶
from neg_env.games.first_price_auction import FirstPriceAuctionGame
from neg_env.spec import TurnOrder
# Defaults: 2+ players, round-robin, random valuations
game = FirstPriceAuctionGame()
# Custom round limit
game = FirstPriceAuctionGame(max_rounds=25)
# Fixed valuations (for controlled experiments)
game = FirstPriceAuctionGame(
max_rounds=25,
valuations={"bidder_a": 60, "bidder_b": 80},
)
# N-player with random turn order
game = FirstPriceAuctionGame(
max_rounds=25,
valuations={"bidder_a": 60, "bidder_b": 80, "bidder_c": 70},
turn_order=TurnOrder.RANDOM,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
max_rounds |
int |
10 |
Maximum rounds before timeout (everyone gets 0). |
valuations |
dict[str, float] \| None |
None |
Fixed valuations per agent. If None, drawn uniformly from [0, 100]. |
turn_order |
TurnOrder |
ROUND_ROBIN |
Turn order mode: ROUND_ROBIN or RANDOM. |
Outcome format¶
{
"payoffs": [
{"agent_id": "bidder_a", "bid": 40, "utility": 20.0}, # winner: 60 - 40
{"agent_id": "bidder_b", "bid": 35, "utility": 0.0}, # loser
{"agent_id": "bidder_c", "bid": 30, "utility": 0.0}, # loser
],
"reason": "auction_resolved", # or "max_rounds_exceeded"
"winner": "bidder_a",
}
Dashboard¶
The dashboard shows auction-specific metrics and per-agent visibility:
- Resolved / Timed out — how many auctions completed vs expired
- Mean bids — average bid per agent (resolved matches only)
- Mean utility (valuation - bid) — average utility per agent (resolved matches only)
- Visibility tooltips — hover the (i) icon on any event to see who can see it. For
submit_bid: "Bid value is sealed (hidden from others)".
Prompts¶
One built-in system prompt for LLM agents:
The prompt instructs the agent to use message_only (not pass) for negotiation, avoid revealing its valuation, bid below valuation to ensure positive utility, and optionally use private messages for collusion.
Example¶
from pathlib import Path
from dotenv import load_dotenv
from neg_env import ExperimentConfig, ExperimentRunner, LangChainNegotiationAgent
from neg_env.prompts import SYSTEM_PROMPT_AUCTION
from neg_env.games.first_price_auction import FirstPriceAuctionGame
from neg_env.spec import TurnOrder
load_dotenv()
agents = [
LangChainNegotiationAgent(
agent_id="bidder_a", provider="openai",
model="gpt-4o-mini", system_prompt=SYSTEM_PROMPT_AUCTION,
),
LangChainNegotiationAgent(
agent_id="bidder_b", provider="openai",
model="gpt-4o-mini", system_prompt=SYSTEM_PROMPT_AUCTION,
),
LangChainNegotiationAgent(
agent_id="bidder_c", provider="openai",
model="gpt-4o-mini", system_prompt=SYSTEM_PROMPT_AUCTION,
),
]
game = FirstPriceAuctionGame(
max_rounds=25,
valuations={"bidder_a": 60, "bidder_b": 80, "bidder_c": 70},
turn_order=TurnOrder.ROUND_ROBIN,
)
config = ExperimentConfig(
game_id="first-price-auction", num_matches=5,
log_directory=Path("./logs/auction"), open_dashboard=True,
)
result = ExperimentRunner(config).run(agents, game=game)