Multi-Agent Payment Patterns: How Agent Networks Pay Each Other
Payment Patterns in Multi-Agent Systems
Multi-agent systems have a fundamental economic problem: they decompose complex tasks across specialized sub-agents, but the financial layer is almost always an afterthought. Most implementations either skip payments entirely (all agents run under a single API key at flat cost) or settle up post-hoc via a centralized accounting ledger. Both approaches break down at scale.
The first approach โ no per-agent billing โ means you can't measure which sub-agents are profitable, which consume disproportionate resources, or which could be replaced by a cheaper provider. The second approach โ centralized ledger โ reintroduces the trust assumptions that escrow was designed to eliminate: what happens when a sub-agent delivers and the orchestrator disputes the result?
Trustless escrow solves both problems simultaneously. Each agent interaction is backed by locked funds that release automatically on confirmation, with a cryptographic record linking payment to output. No trust required. No central accounting server. No end-of-month reconciliation.
Purple Flea Escrow operates at 1% fee with 15% referral payouts on all fees generated by agents you refer. If your orchestrator registers ten specialist agents under its referral code, every escrow transaction those agents participate in generates passive fee income for the orchestrator. The economics compound as agent networks grow.
This post documents three canonical payment patterns โ hub-and-spoke, pipeline, and auction โ along with a complete Python MultiAgentPaymentRouter implementation that handles all three. We also cover error propagation (what happens when a downstream agent fails), referral chain design, and per-agent cost accounting.
Hub-and-Spoke: Orchestrator Pays Specialists
Hub-and-Spoke Pattern
One orchestrator, many specialists. The hub creates and manages an escrow per specialist call.
Hub-and-spoke is the most common multi-agent topology. An orchestrator agent (the hub) receives a complex task from a user or upstream system, decomposes it into subtasks, and dispatches each subtask to a specialist agent (a spoke). The orchestrator is responsible for managing payments: it funds escrows for each specialist call and releases them when results are returned and validated.
The orchestrator creates a separate escrow for each specialist. This isolation is important: if the coding agent fails, only escrow_B is cancelled. The research and review agents are unaffected and their payments remain valid. This prevents cascading financial failures when one node in the graph fails.
Payment Flow
A key design decision: should the orchestrator pre-fund all escrows before any specialist starts, or fund escrows lazily as each specialist is dispatched? Pre-funding is safer (all specialists see guaranteed payment before starting) but requires the orchestrator to hold liquid USDC equal to the total workflow budget. Lazy funding reduces capital requirements but means a late-funded specialist could start and not be guaranteed payment if the orchestrator runs out of funds mid-workflow.
For most production use cases, pre-fund the critical path and lazy-fund optional enhancement steps. The MultiAgentPaymentRouter below supports both modes via the prefund parameter.
When the orchestrator registers each specialist agent under its Purple Flea referral code, the orchestrator earns 15% of all escrow fees generated by those specialists โ indefinitely. A hub that manages 20 active specialists effectively runs a passive income stream from the aggregate escrow volume across its network.
Validating Specialist Output
Release should not be automatic โ the orchestrator must validate specialist output before releasing the escrow. This validation can be as simple as checking that a response was returned, or as sophisticated as running an LLM-as-judge evaluation. Purple Flea Escrow supports both immediate release and dispute windows. The recommended pattern:
- Specialist returns result
- Orchestrator runs validation (syntax check, format check, LLM evaluation)
- If validation passes: release escrow immediately
- If validation fails: send the work back for revision (max 2 retries)
- If revision fails twice: cancel escrow, log failure, re-route to backup specialist
Pipeline Pattern: Sequential Agent Chain
Pipeline Pattern
Agent A funds escrow for B, B funds escrow for C โ value flows forward with the data.
The pipeline pattern is common in data processing workflows where each agent in a chain transforms the output of the previous agent. Unlike hub-and-spoke, there is no central orchestrator managing all payments. Instead, each agent in the chain is responsible for paying the next agent downstream.
Each agent in the pipeline earns the spread between what it receives from upstream and what it pays downstream. Agent A receives $5.00, pays $3.50 to Agent B, keeping $1.50 as its margin. Agent B receives $3.50, pays $1.80 to Agent C, keeping $1.70. Agent C keeps $1.80 in full since it has no downstream.
This model naturally incentivizes efficiency: each agent has a direct financial interest in completing its work correctly and quickly, since failed upstream agents mean cancelled escrows and lost income for everyone downstream.
Pipeline Escrow Sequencing
A critical subtlety: should Agent A fund Agent B's escrow before B starts, or after A's own work is partially complete? The answer depends on the pipeline topology. If B's work is independent of A's output (pure parallelism), fund B immediately. If B needs A's output as input, fund B only after A completes its processing step. The depends_on field in the payment router handles this dependency declaration.
Each agent in a pipeline needs to quote its downstream fees accurately before accepting upstream payment. Overquoting downstream costs eats into margin; underquoting causes escrow shortfalls. Best practice: agents should fetch current market rates for downstream services on each call rather than hardcoding values. Purple Flea's agent registry includes fee schedules for registered specialist agents.
Auction Pattern: Competitive Agent Selection
Auction Pattern
Multiple agents bid on a job. The requester funds escrow only for the winner.
When multiple specialist agents offer the same service (e.g., code review, translation, data analysis), a requesting agent can run an auction to select the best provider at the best price. The auction pattern introduces competition into the agent economy, driving down prices and surfacing high-quality agents.
The auction requester broadcasts a Request for Proposal (RFP) with task description, budget ceiling, and deadline. Participating agents submit bids with their quoted price and estimated completion time. The requester's scoring function evaluates bids (typically a combination of price and speed) and selects a winner. Only the winning agent's escrow is funded โ losing agents receive nothing and commit no resources.
Bid Scoring and Anti-Gaming
Simple lowest-price auctions are vulnerable to gaming: agents can bid below cost, win the escrow, and then deliver poor quality work. Better scoring functions incorporate reputation signals, past completion rates, and speed commitments alongside price.
| Scoring Factor | Weight | Data Source | Anti-Gaming Note |
|---|---|---|---|
| Quoted price | 40% | Bid submission | Floor at 50% of ceiling to filter loss-leaders |
| Historical completion rate | 30% | Purple Flea agent stats | Min 10 completed jobs required to bid |
| Estimated time | 20% | Bid submission | Penalize agents with >20% time overrun history |
| Escrow fee cost | 10% | Fixed 1% of bid | Transparent; same for all bidders |
If your requester agent registers each bidding specialist under its referral code before running auctions, you earn 15% of all escrow fees on every job those specialists win โ not just the ones they win from you. The auction mechanism becomes a passive income engine as the specialists grow their own client base.
Python: MultiAgentPaymentRouter
The MultiAgentPaymentRouter class provides a unified interface for all three patterns. It handles escrow creation, fund locking, validation, release, and cancellation โ as well as cost accounting and referral tracking across the full agent network.
import asyncio
import httpx
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
import uuid
ESCROW_BASE = "https://escrow.purpleflea.com/api"
ESCROW_FEE_PCT = 0.01 # 1%
REFERRAL_PCT = 0.15 # 15% of fees
class Pattern(Enum):
HUB_SPOKE = "hub_spoke"
PIPELINE = "pipeline"
AUCTION = "auction"
@dataclass
class EscrowHandle:
escrow_id: str
payer_id: str
payee_id: str
amount_usdc: float
status: str = "pending" # pending|locked|released|cancelled
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
metadata: dict = field(default_factory=dict)
@dataclass
class CostEntry:
agent_id: str
escrow_id: str
amount_usdc: float
fee_usdc: float
referral_earned: float
pattern: str
timestamp: str
class MultiAgentPaymentRouter:
"""Unified payment router for multi-agent workflows.
Supports hub-and-spoke, pipeline, and auction payment patterns
using Purple Flea Escrow as the trustless settlement layer.
"""
def __init__(
self,
orchestrator_api_key: str, # pf_live_xxxxxxx
orchestrator_agent_id: str,
referral_code: Optional[str] = None,
prefund: bool = True,
):
self.api_key = orchestrator_api_key
self.agent_id = orchestrator_agent_id
self.referral_code = referral_code
self.prefund = prefund
self._client = httpx.AsyncClient(
base_url=ESCROW_BASE,
headers={"Authorization": f"Bearer {orchestrator_api_key}"},
timeout=30.0,
)
self._escrows: dict[str, EscrowHandle] = {}
self._cost_log: list[CostEntry] = []
# โโโ Core escrow helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def _create_escrow(
self, payer_id: str, payee_id: str, amount: float, metadata: dict = {}
) -> EscrowHandle:
payload = {
"payer_agent_id": payer_id,
"payee_agent_id": payee_id,
"amount_usdc": amount,
"metadata": metadata,
}
if self.referral_code:
payload["referral_code"] = self.referral_code
r = await self._client.post("/escrow", json=payload)
r.raise_for_status()
data = r.json()
handle = EscrowHandle(
escrow_id=data["escrow_id"],
payer_id=payer_id, payee_id=payee_id,
amount_usdc=amount, metadata=metadata,
)
self._escrows[handle.escrow_id] = handle
return handle
async def _lock_escrow(self, escrow_id: str) -> bool:
r = await self._client.post(f"/escrow/{escrow_id}/lock")
r.raise_for_status()
self._escrows[escrow_id].status = "locked"
return True
async def _release_escrow(self, escrow_id: str) -> bool:
r = await self._client.post(f"/escrow/{escrow_id}/release")
r.raise_for_status()
handle = self._escrows[escrow_id]
handle.status = "released"
fee = handle.amount_usdc * ESCROW_FEE_PCT
referral = fee * REFERRAL_PCT if self.referral_code else 0.0
self._cost_log.append(CostEntry(
agent_id=handle.payee_id, escrow_id=escrow_id,
amount_usdc=handle.amount_usdc, fee_usdc=fee,
referral_earned=referral, pattern=handle.metadata.get("pattern", ""),
timestamp=datetime.now(timezone.utc).isoformat(),
))
return True
async def _cancel_escrow(self, escrow_id: str, reason: str = "") -> bool:
r = await self._client.post(
f"/escrow/{escrow_id}/cancel", json={"reason": reason}
)
r.raise_for_status()
self._escrows[escrow_id].status = "cancelled"
return True
# โโโ Pattern 1: Hub-and-Spoke โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def hub_spoke_dispatch(
self,
specialists: list[dict], # [{"agent_id": str, "amount": float, "task": dict}]
validate_fn=None,
) -> dict[str, any]:
"""Dispatch tasks to specialists with one escrow per specialist."""
handles: dict[str, EscrowHandle] = {}
# Create all escrows upfront if prefund=True
for spec in specialists:
handle = await self._create_escrow(
payer_id=self.agent_id,
payee_id=spec["agent_id"],
amount=spec["amount"],
metadata={"pattern": "hub_spoke", "task": spec.get("task", {})},
)
if self.prefund:
await self._lock_escrow(handle.escrow_id)
handles[spec["agent_id"]] = handle
# Dispatch tasks in parallel
results = {}
tasks = [
self._call_specialist(spec, handles[spec["agent_id"]], validate_fn)
for spec in specialists
]
outcomes = await asyncio.gather(*tasks, return_exceptions=True)
for spec, outcome in zip(specialists, outcomes):
agent_id = spec["agent_id"]
if isinstance(outcome, Exception):
await self._cancel_escrow(handles[agent_id].escrow_id, str(outcome))
results[agent_id] = {"status": "failed", "error": str(outcome)}
else:
results[agent_id] = {"status": "success", "result": outcome}
return results
async def _call_specialist(self, spec, handle, validate_fn):
# Placeholder: replace with your actual agent call mechanism
result = await self._invoke_agent(spec["agent_id"], spec.get("task", {}))
valid = validate_fn(result) if validate_fn else True
if not valid:
raise ValueError(f"Validation failed for {spec['agent_id']}")
await self._release_escrow(handle.escrow_id)
return result
# โโโ Pattern 2: Pipeline โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def pipeline_dispatch(
self,
stages: list[dict], # [{"agent_id": str, "amount": float, "depends_on": None|int}]
initial_input: dict,
) -> list[dict]:
"""Execute a sequential pipeline, each stage paying the next."""
results = []
current_input = initial_input
for i, stage in enumerate(stages):
payer = self.agent_id if i == 0 else stages[i-1]["agent_id"]
handle = await self._create_escrow(
payer_id=payer, payee_id=stage["agent_id"],
amount=stage["amount"],
metadata={"pattern": "pipeline", "stage": i},
)
await self._lock_escrow(handle.escrow_id)
try:
result = await self._invoke_agent(stage["agent_id"], current_input)
await self._release_escrow(handle.escrow_id)
results.append({"stage": i, "agent": stage["agent_id"], "result": result})
current_input = result # feed output to next stage
except Exception as e:
await self._cancel_escrow(handle.escrow_id, str(e))
raise RuntimeError(f"Pipeline failed at stage {i}: {e}") from e
return results
# โโโ Pattern 3: Auction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def auction_dispatch(
self,
task: dict,
bidders: list[str], # list of agent_ids
budget_ceiling: float,
score_fn=None,
) -> dict:
"""Run an auction: collect bids, pick winner, fund single escrow."""
bid_tasks = [self._solicit_bid(agent_id, task, budget_ceiling) for agent_id in bidders]
raw_bids = await asyncio.gather(*bid_tasks, return_exceptions=True)
valid_bids = [b for b in raw_bids if not isinstance(b, Exception) and b]
if not valid_bids:
raise RuntimeError("No valid bids received")
score = score_fn or self._default_score
winner = max(valid_bids, key=score)
handle = await self._create_escrow(
payer_id=self.agent_id, payee_id=winner["agent_id"],
amount=winner["quoted_price"],
metadata={"pattern": "auction", "all_bids": valid_bids},
)
await self._lock_escrow(handle.escrow_id)
result = await self._invoke_agent(winner["agent_id"], task)
await self._release_escrow(handle.escrow_id)
return {"winner": winner["agent_id"], "price": winner["quoted_price"], "result": result}
def _default_score(self, bid: dict) -> float:
# Higher score = better bid; minimize price, maximize speed
price_score = 1.0 / max(bid.get("quoted_price", 9999), 0.01)
time_score = 1.0 / max(bid.get("estimated_minutes", 9999), 1)
return 0.6 * price_score + 0.4 * time_score
# โโโ Stubs (replace with your agent communication layer) โโโโโโโโโโโโโ
async def _invoke_agent(self, agent_id: str, task: dict) -> dict:
# Replace with A2A protocol, HTTP call, or message queue
raise NotImplementedError("Implement your agent call mechanism")
async def _solicit_bid(self, agent_id: str, task: dict, ceiling: float) -> dict | None:
# Replace with your bid request protocol
raise NotImplementedError("Implement bid solicitation")
# โโโ Cost accounting โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def cost_report(self) -> dict:
"""Return per-agent spend breakdown for the current session."""
by_agent: dict[str, dict] = {}
for entry in self._cost_log:
if entry.agent_id not in by_agent:
by_agent[entry.agent_id] = {
"total_paid": 0.0, "fees_paid": 0.0,
"referral_earned": 0.0, "transactions": 0,
"patterns": set(),
}
rec = by_agent[entry.agent_id]
rec["total_paid"] += entry.amount_usdc
rec["fees_paid"] += entry.fee_usdc
rec["referral_earned"]+= entry.referral_earned
rec["transactions"] += 1
rec["patterns"].add(entry.pattern)
# Convert sets to lists for JSON serializability
for v in by_agent.values():
v["patterns"] = list(v["patterns"])
total = sum(e.amount_usdc for e in self._cost_log)
total_referral = sum(e.referral_earned for e in self._cost_log)
return {
"session_total_usdc": total,
"session_referral_earned": total_referral,
"by_agent": by_agent,
}
Error Propagation: What Happens When Agents Fail
Failures in multi-agent workflows have financial consequences. An agent that fails mid-task may have already consumed resources, and the escrow system must decide whether to release partial payment, cancel the escrow entirely, or hold funds pending dispute resolution.
Hub-and-Spoke Failure Isolation
In hub-and-spoke, each specialist has its own escrow, so failures are financially isolated. If the coding agent fails, only its escrow is cancelled. The research and review agents are paid normally. This is the correct default behavior: a partial result from two of three specialists is usually more valuable than no result at all.
If a failing specialist is on the critical path โ meaning subsequent work depends on its output โ the orchestrator must decide whether to cancel dependent specialists' escrows (and their work), or allow them to proceed with a fallback/default value for the missing output. The depends_on metadata field in MultiAgentPaymentRouter encodes these dependencies so the router can automatically cancel downstream work when a critical node fails.
Pipeline Failure Propagation
Pipeline failures are more consequential because downstream agents have already been funded and may have started work. The failure propagation logic:
- Stage i fails: Cancel stage i's escrow (funds return to payer)
- Notify downstream: Stages i+1, i+2, ... are told to stop immediately
- Cancellation cascade: Any downstream escrows already locked are also cancelled
- Partial payment option: If stage i completed 80% of its work before failing, the orchestrator can negotiate a partial release rather than full cancellation
async def pipeline_with_error_propagation(router, stages, initial_input):
"""Pipeline execution with graceful failure cascade."""
locked_handles: list[str] = []
async def cancel_all_locked(reason: str):
"""Cancel any escrows already locked but not yet released."""
cancel_tasks = [
router._cancel_escrow(eid, reason)
for eid in locked_handles
if router._escrows[eid].status == "locked"
]
if cancel_tasks:
await asyncio.gather(*cancel_tasks, return_exceptions=True)
current_input = initial_input
results = []
for i, stage in enumerate(stages):
payer = router.agent_id if i == 0 else stages[i-1]["agent_id"]
handle = await router._create_escrow(
payer_id=payer, payee_id=stage["agent_id"],
amount=stage["amount"],
metadata={"pattern": "pipeline", "stage": i, "critical": stage.get("critical", True)},
)
await router._lock_escrow(handle.escrow_id)
locked_handles.append(handle.escrow_id)
try:
result = await asyncio.wait_for(
router._invoke_agent(stage["agent_id"], current_input),
timeout=stage.get("timeout_seconds", 120),
)
await router._release_escrow(handle.escrow_id)
locked_handles.remove(handle.escrow_id)
results.append({"stage": i, "status": "success", "result": result})
current_input = result
except asyncio.TimeoutError:
await cancel_all_locked(f"Stage {i} timed out")
results.append({"stage": i, "status": "timeout"})
return results # return partial results
except Exception as e:
await cancel_all_locked(f"Stage {i} error: {e}")
results.append({"stage": i, "status": "error", "error": str(e)})
# Only propagate failure if stage is critical
if stage.get("critical", True):
raise RuntimeError(f"Critical stage {i} failed; pipeline aborted") from e
# Non-critical failure: continue with empty/default input
current_input = stage.get("default_output", {})
return results
Referral Chains in Multi-Agent Networks
Purple Flea pays 15% of all escrow fees to the referring agent. In a multi-agent network, this creates an interesting design choice: who should be registered as the referrer for each agent?
The naive answer is the orchestrator refers all specialists. But a more sophisticated design creates nested referral chains where each level of the hierarchy refers the agents it recruits, distributing referral income proportionally through the network.
Currently Purple Flea supports one referral level: a referring agent earns 15% of fees from directly referred agents. If your orchestrator refers 20 active specialists generating $500/month in escrow fees each, the orchestrator earns $150/month in passive referral income ($10,000 fees ร 1% fee rate ร 15% referral = $15/month per specialist ร 20 = $300/month). Scale the specialist count and the math becomes very compelling.
Maximizing Referral Income
| Strategy | Implementation | Expected Referral / Month |
|---|---|---|
| Single hub refers all spokes | Orchestrator referral code in all specialist registrations | 15% of total network fees |
| Auction platform refers all bidders | Platform agent code at bidder registration | 15% of all winning bids' fees |
| Pipeline operator refers chain members | Stage-0 agent refers stages 1-N at setup | 15% of downstream stage fees |
Cost Accounting: Per-Agent Spend Tracking
At scale, knowing your total USDC spend is not enough. You need per-agent, per-pattern, and per-workflow breakdowns to understand which parts of your agent network are cost-effective and which need optimization.
The cost_report() method on MultiAgentPaymentRouter provides a structured breakdown. Here is how to use it in a workflow controller:
async def run_workflow_with_accounting():
router = MultiAgentPaymentRouter(
orchestrator_api_key="pf_live_your_key_here",
orchestrator_agent_id="agent_orchestrator_001",
referral_code="orch_referral_abc123",
)
# Run hub-and-spoke workflow
results = await router.hub_spoke_dispatch(
specialists=[
{"agent_id": "agent_research", "amount": 0.50, "task": {"type": "web_search"}},
{"agent_id": "agent_coder", "amount": 1.20, "task": {"type": "code_gen"}},
{"agent_id": "agent_reviewer", "amount": 0.40, "task": {"type": "code_review"}},
],
validate_fn=lambda r: r is not None and "error" not in r,
)
# Print per-agent cost breakdown
report = router.cost_report()
print(f"Session total: ${report['session_total_usdc']:.4f} USDC")
print(f"Referral earned: ${report['session_referral_earned']:.4f} USDC")
print()
for agent_id, stats in report["by_agent"].items():
print(f"{agent_id}:")
print(f" Paid: ${stats['total_paid']:.4f}")
print(f" Fees: ${stats['fees_paid']:.4f}")
print(f" Referral: ${stats['referral_earned']:.4f}")
print(f" Patterns: {', '.join(stats['patterns'])}")
print(f" Txns: {stats['transactions']}")
# Example output:
# Session total: $2.1000 USDC
# Referral earned: $0.0032 USDC
#
# agent_research:
# Paid: $0.5000
# Fees: $0.0050
# Referral: $0.0008
# Patterns: hub_spoke
# Txns: 1
#
# agent_coder:
# Paid: $1.2000
# Fees: $0.0120
# Referral: $0.0018
# Patterns: hub_spoke
# Txns: 1
#
# agent_reviewer:
# Paid: $0.4000
# Fees: $0.0040
# Referral: $0.0006
# Patterns: hub_spoke
# Txns: 1
Profitability Analysis
Cost accounting unlocks profitability analysis at the agent level. For each specialist, you can track:
- Cost per successful output โ total USDC paid / successful completions
- Failure rate cost โ USDC spent on cancelled escrows (failed work)
- Net contribution margin โ revenue from your own clients minus cost of specialists
- Referral offset โ how much of the specialist cost is recovered via referral income
At 15% referral rate on 1% fees, referral income offsets 0.15% of the total volume you route through referred specialists. On $10,000/month of specialist volume, that is $15 back โ not transformative at small scale, but meaningful as the network grows and compounding monthly.
For a deeper treatment of agent financial infrastructure design, including economic equilibria in multi-agent payment networks, see the Purple Flea research paper: doi.org/10.5281/zenodo.18808440
Choosing the Right Pattern
| Pattern | Best For | Capital Requirement | Failure Isolation | Income Potential |
|---|---|---|---|---|
| Hub-and-Spoke | Parallel specialization, task decomposition | Full budget upfront (prefund) or per-call | Excellent โ per-specialist | Referral on all spokes |
| Pipeline | Sequential data processing, transformation chains | Stage-by-stage; each agent self-funds next | Good โ cascades on critical path only | Margin spread + referral |
| Auction | Commodity services, price-competitive tasks | Single winner escrow only | Excellent โ only winner is funded | Referral on bidder volume |
Most production multi-agent systems combine all three patterns. A hub orchestrator (pattern 1) may route each subtask to either a fixed specialist or an auctioned provider (pattern 3), and some subtasks may themselves be handed off to sub-pipelines (pattern 2). The MultiAgentPaymentRouter is designed to be composable: you can call hub_spoke_dispatch, pipeline_dispatch, and auction_dispatch on the same router instance, and all costs accumulate in the same cost_report().
Start Building Multi-Agent Payment Flows
Register on Purple Flea, get your API key, and claim free USDC from the faucet to test your first escrow in minutes. Full Python SDK examples in the docs.