Agent Payment Protocol
The Agent Payment Protocol standardizes how two agents negotiate and finalize a payment. It defines message types, valid interaction paths, and clear roles for a buyer and a seller. This guide explains the models, interaction rules, and shows runnable examples for both roles.
Understanding the Payment Protocol
1. Core Models
Funds
class Funds(Model):
# amount of funds
amount: str
# currency of the funds (USDC, FET, etc.)
currency: str
# payment method (skyfire, fet_direct)
payment_method: str = "fet_direct"
- amount: amount expressed as a string (supports arbitrary formats/precisions)
- currency: the asset or token symbol
- payment_method: payment rail/method identifier
RequestPayment
class RequestPayment(Model):
# funds accepted for the requested payment
accepted_funds: list[Funds]
# recipient address or identifier
recipient: str
# deadline for the payment request in seconds
deadline_seconds: int
# optional reference for the payment
reference: str | None = None
# optional description of the payment
description: str | None = None
# optional metadata for the payment
metadata: dict[str, str | dict[str, str]] | None = None
- accepted_funds: one or more acceptable fund options the buyer proposes
- recipient: where funds should be sent (address, handle, etc.)
- deadline_seconds: validity window of the request
- Optional: reference, description, metadata
RejectPayment
class RejectPayment(Model):
# optional reason for rejecting the payment
reason: str | None = None
- Sent by the seller to decline a request
CommitPayment
class CommitPayment(Model):
# funds to be paid
funds: Funds
# recipient address or identifier
recipient: str
# unique transaction token or hash
transaction_id: str
# optional reference for the payment
reference: str | None = None
# optional description of the payment
description: str | None = None
# optional metadata for the payment
metadata: dict[str, str | dict[str, str]] | None = None
- Seller’s commitment response, specifying the exact funds and a transaction_id to track the payment
CancelPayment
class CancelPayment(Model):
# unique transaction token or hash
transaction_id: str | None = None
# optional reason for canceling the payment
reason: str | None = None
- Buyer cancels an in-flight or accepted payment
CompletePayment
class CompletePayment(Model):
# unique transaction token or hash
transaction_id: str | None = None
- Buyer declares the payment completed
2. Protocol Specification
payment_protocol_spec = ProtocolSpecification(
name="AgentPaymentProtocol",
version="0.1.0",
interactions={
RequestPayment: {CommitPayment, RejectPayment},
CommitPayment: {CompletePayment, CancelPayment},
CompletePayment: set(),
CancelPayment: set(),
RejectPayment: set(),
},
roles={
"seller": {RequestPayment, CancelPayment, CompletePayment},
"buyer": {CommitPayment, RejectPayment},
},
)
- Interactions: valid next messages per message type
- Roles: which message types each role is allowed to send
3. Basic Payment Flow
- Seller sends
RequestPayment
to Buyer - Buyer responds with either
CommitPayment
orRejectPayment
- If committed, Seller sends
CompletePayment
(orCancelPayment
)
Using the Payment Protocol
Import the models and specification from uagents_core.contrib.protocols.payment
:
from uagents_core.contrib.protocols.payment import (
Funds,
RequestPayment,
RejectPayment,
CommitPayment,
CancelPayment,
CompletePayment,
payment_protocol_spec,
)
Note: Since the specification defines roles, instantiate the protocol with the appropriate role
to avoid locked-spec errors.
Example Agents
Below are two minimal agents demonstrating the Buyer and Seller roles using the protocol.
Seller Agent
import os
from uuid import uuid4
from uagents import Agent, Protocol, Context
from uagents_core.contrib.protocols.payment import (
Funds,
RequestPayment,
CommitPayment,
RejectPayment,
CompletePayment,
payment_protocol_spec,
)
SELLER_PORT = int(os.getenv("SELLER_PORT", "8091"))
BUYER_ADDRESS = "agent1q...replace_with_buyer_address..."
seller = Agent(name="seller", port=SELLER_PORT, endpoint=[f"http://localhost:{SELLER_PORT}/submit"])
payment_proto = Protocol(spec=payment_protocol_spec, role="seller")
@seller.on_event("startup")
async def startup(ctx: Context):
ctx.logger.info(f"Seller address: {ctx.agent.address}")
req = RequestPayment(
accepted_funds=[Funds(currency="USDC", amount="0.001", payment_method="skyfire")],
recipient=ctx.agent.address,
deadline_seconds=300,
reference=str(uuid4()),
description="demo payment",
metadata={},
)
await ctx.send(BUYER_ADDRESS, req)
@payment_proto.on_message(CommitPayment)
async def on_commit(ctx: Context, sender: str, msg: CommitPayment):
ctx.logger.info(f"Seller received CommitPayment: {msg}")
await ctx.send(sender, CompletePayment(transaction_id=msg.transaction_id))
@payment_proto.on_message(RejectPayment)
async def on_reject(ctx: Context, sender: str, msg: RejectPayment):
ctx.logger.info(f"Buyer rejected: {msg.reason}")
seller.include(payment_proto, publish_manifest=True)
if __name__ == "__main__":
seller.run()
Buyer Agent
import os
from uagents import Agent, Protocol, Context
from uagents_core.contrib.protocols.payment import (
Funds,
RequestPayment,
CommitPayment,
RejectPayment,
CompletePayment,
CancelPayment,
payment_protocol_spec,
)
BUYER_PORT = int(os.getenv("BUYER_PORT", "8092"))
BUYER_MODE = os.getenv("BUYER_MODE", "commit").lower() # commit | reject
buyer = Agent(name="buyer", port=BUYER_PORT, endpoint=[f"http://localhost:{BUYER_PORT}/submit"])
payment_proto = Protocol(spec=payment_protocol_spec, role="buyer")
@payment_proto.on_message(RequestPayment)
async def on_request(ctx: Context, sender: str, msg: RequestPayment):
ctx.logger.info(f"Buyer received RequestPayment: {msg}")
if not msg.accepted_funds:
await ctx.send(sender, RejectPayment(reason="no accepted funds provided"))
return
selected = msg.accepted_funds[0]
if BUYER_MODE == "reject":
await ctx.send(sender, RejectPayment(reason="demo reject"))
return
commit = CommitPayment(
funds=Funds(currency=selected.currency, amount=selected.amount, payment_method=selected.payment_method),
recipient=msg.recipient,
transaction_id="demo-txn-001",
reference=msg.reference,
description=msg.description,
metadata=msg.metadata or {},
)
await ctx.send(sender, commit)
@payment_proto.on_message(CompletePayment)
async def on_complete(ctx: Context, sender: str, msg: CompletePayment):
ctx.logger.info(f"Buyer received CompletePayment: {msg}")
@payment_proto.on_message(CancelPayment)
async def on_cancel(ctx: Context, sender: str, msg: CancelPayment):
ctx.logger.info(f"Buyer received CancelPayment: {msg}")
buyer.include(payment_proto, publish_manifest=True)
if __name__ == "__main__":
buyer.run()
Running the Agents
To run locally, initialize explicit ports as needed (optional):
# Seller
seller = Agent(name="seller", port=8002, endpoint=["http://localhost:8002/submit"])
# Buyer
buyer = Agent(name="buyer", port=8003, endpoint=["http://localhost:8003/submit"])
- Start the Seller first and copy its address from logs.
- Paste the seller address into
SELLER_ADDRESS
inbuyer.py
. - Start the Buyer.
You should see the Buyer send a RequestPayment
, the Seller respond with a CommitPayment
, and the Buyer send a CompletePayment
.
To adapt for your own project structure, import from uagents_core.contrib.protocols.payment
as shown above, or vendor the models/spec locally if needed.
Expected Output
When both agents are running, you should see logs similar to the following.
Buyer logs
INFO: [demo_buyer]: Starting agent with address: agent1qdyxc5qr9lwawcdshwy9za8gen9l72llevpuv590xt2vmh07ef6jcjq7u6m
INFO: [demo_buyer]: Agent inspector available at https://agentverse.ai/inspect/?uri=http%3A//127.0.0.1%3A8092&address=agent1qdyxc5qr9lwawcdshwy9za8gen9l72llevpuv590xt2vmh07ef6jcjq7u6m
INFO: [demo_buyer]: Starting server on http://0.0.0.0:8092 (Press CTRL+C to quit)
INFO: [uagents.registration]: Registration on Almanac API successful
INFO: [uagents.registration]: Almanac contract registration is up to date!
INFO: [demo_buyer]: Manifest published successfully: AgentPaymentProtocol
INFO: [demo_buyer]: [buyer] got RequestPayment from agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm: accepted_funds=[Funds(amount='0.001', currency='USDC', payment_method='skyfire')] recipient='agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm' deadline_seconds=300 reference='f16bb6eb-3643-4990-badd-f456122a489d' description='demo-simple payment' metadata={}
INFO: [demo_buyer]: [buyer] sent CommitPayment
INFO: [demo_buyer]: [buyer] received CompletePayment from agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm: transaction_id='demo-txn-001'
Seller logs
INFO: [demo_seller]: Starting agent with address: agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm
INFO: [demo_seller]: [seller] starting up…
INFO: [demo_seller]: [seller] sending RequestPayment to agent1qdyxc5qr9lwawcdshwy9za8gen9l72llevpuv590xt2vmh07ef6jcjq7u6m: accepted_funds=[Funds(amount='0.001', currency='USDC', payment_method='skyfire')] recipient='agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm' deadline_seconds=300 reference='f16bb6eb-3643-4990-badd-f456122a489d' description='demo-simple payment' metadata={}
INFO: [demo_seller]: Agent inspector available at https://agentverse.ai/inspect/?uri=http%3A//127.0.0.1%3A8091&address=agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm
INFO: [demo_seller]: Starting server on http://0.0.0.0:8091 (Press CTRL+C to quit)
INFO: [demo_seller]: Manifest published successfully: AgentPaymentProtocol
INFO: [uagents.registration]: Registration on Almanac API successful
INFO: [uagents.registration]: Almanac contract registration is up to date!
INFO: [demo_seller]: [seller] got CommitPayment from agent1qdyxc5qr9lwawcdshwy9za8gen9l72llevpuv590xt2vmh07ef6jcjq7u6m: funds=Funds(amount='0.001', currency='USDC', payment_method='skyfire') recipient='agent1qv708v7970lzq6gmunvrz9xr4erfxamrc7mw9tf0ffj50nfc7tpkkaa4lrm' transaction_id='demo-txn-001' reference='f16bb6eb-3643-4990-badd-f456122a489d' description='demo-simple payment' metadata={}
INFO: [demo_seller]: [seller] sent CompletePayment
Conclusion
The Payment Protocol provides a clear, role-driven workflow for negotiating and finalizing payments between agents. By defining message types, valid interactions, and explicit roles, it helps you implement predictable flows: the seller initiates with RequestPayment
, the buyer either CommitPayment
s or RejectPayment
s, and the seller completes or cancels as appropriate.
- Always instantiate your protocol with the correct
role
to match the allowed messages. - Use the examples to bootstrap local testing, then adapt handlers to perform real transfers, persistence, retries, and idempotency.
- For complementary patterns, see the Agent Chat Protocol and the agent setup notes in Agent Creation.
With this foundation, you can extend validation, add signatures or receipts, and integrate with on-chain or off-chain payment rails as needed.