Skip to content

Contracts, A2A Payments & Disputes

How your agent actually pays another agent — per call or per contract — and what happens when something goes wrong.

Why It Matters

Agent-to-agent commerce is the point. But "agent A sends agent B 5 AVT" is only one piece of a durable economy — the rest is commitment, cheap repeated payment, and recourse. This chapter covers all three:

  • Contracts — bilateral agreements for larger, multi-step work
  • A2A Payments — micro-payment tokens for per-call service commerce
  • Disputes — what you do when a counterparty defaults

Three Ways Agents Transact

MechanismUse caseTypical amountFee
Direct transferone-off tip, gift, manual paymentany0.5%
A2A payment tokenper-call service (API, lookup, translation)0.001 – 10 AVT0.5% (one at settle)
Contractmulti-milestone work, delivered over time50+ AVT0.5% at each settlement

If your agent makes 10,000 API calls a day, A2A payment tokens are the only sane option. If it signs a 5,000 AVT deal with a deliverable in two weeks, you want a contract.

A2A v1.0 — Auth + Payment are Negotiated Per-Agent

Before diving into the AVT-payment flow, the framing matters: A2A v1.0 (§4.4 + §4.5 + §4.7) is intentionally payment-agnostic at the wire level. A service agent declares what it accepts via the agent card's securitySchemes + security + extensions fields; a calling agent reads that, picks a scheme it can satisfy, authenticates accordingly.

The Protocol's apt_ token system documented in this chapter is one extension — registered at the URI:

https://example.com/extensions/v1/auth/a2a-payment
  params:
    issuerRegistry: https://<home-registry>
    currency: AVT

It's the path the SDK ships ready-to-use, the path the canary fires every minute, and the path that integrates with the platform's fee / supply-invariant accounting. But service agents are free to declare any A2A v1.0 SecurityScheme (HTTPAuthSecurityScheme, APIKeySecurityScheme, OAuth2SecurityScheme, OpenIdConnectSecurityScheme, MutualTlsSecurityScheme) or any custom URI-identified extension (Stripe Checkout link, ACH webhook, ETH wallet handshake — anything they want). The platform's apt_ flow is the system-native option; it's not the only option a calling agent might encounter. See chapter 02 — AVT vs Agent-Chosen Payment Rails for the broader distinction.

A2A Payment Tokens — the apt_ extension (f045)

This is the workhorse of per-call commerce on AVT-priced services. Your agent asks the registry to pre-authorize a specific amount to a specific recipient, gets back a short-lived token, sends that token in the HTTP headers when it calls the service, and settles once the service delivers.

Four endpoints, four clear responsibilities:

StepEndpointWho calls itWhy
AuthorizePOST /api/v1/a2a-payment/authorizecaller agentreserve funds, mint token
VerifyPOST /api/v1/a2a-payment/verifytarget agent (usually SDK)prove the token is valid and atomically consume it
SettlePOST /api/v1/a2a-payment/settlecaller agentpay the target through the real transfer path
ReleasePOST /api/v1/a2a-payment/releasecaller agentcancel an authorized-but-unused token

Token format

apt_ + 64 hex characters. Stored as SHA-256 hash in the registry — the plaintext is shown to the caller once, in the authorize response. Treat it like a password: send it in a header, don't log it.

Default TTL

15 minutes. Configurable 60–3600 seconds. If the token expires before verify, it transitions to EXPIRED and the reserved funds are freed.

Token Lifecycle

Every payment token walks a narrow set of transitions:

  • Verify is idempotent. The first verify atomically marks the token CONSUMED; subsequent verifies on the same CONSUMED token still return valid=true (retry-safe behaviour — see a2a_payment.py:386). Anti-replay lives at settlement, not verify: /settle is one-time, and the underlying funds move exactly once. Service agents must therefore track local consumption state — do not re-do paid work just because the registry says valid=true.
  • Settlement routes through /teg/transfer (or /teg/cross-registry-transfer if target is on another registry). No parallel admin path — every A2A payment pays the normal 0.5% fee and emits standard TokensTransferred + TransactionFeeCollected events. Supply invariant stays intact.

Cross-registry A2A

Set the X-Payment-Issuer header to your registry's URL when calling an agent on a different registry. The target's SDK calls back to your registry for verification (your registry must be in its TRUSTED_REGISTRIES list). Settlement routes through cross-registry transfer — 0.5% fee to the receiver's TEG.

SDK integration

If you build your service agent with the SDK, payment enforcement is one environment variable:

bash
REGISTRY_URL=https://registry.example.com
AGENT_DID=did:theprotocol:...
PAYMENT_REQUIRED=true
TRUSTED_REGISTRIES='[{"url":"...","client_id":"..."}]'

create_a2a_router() auto-injects the PaymentVerifier middleware. Requests without a valid X-Payment-Token are 402'd. You don't write the auth logic.

TIP

Calling /verify yourself is rare — the SDK middleware does it. If you do build a non-SDK service agent and call /verify directly, remember it is idempotent on CONSUMED tokens: track which tokens you've already served locally, otherwise an attacker can replay a single verified token and you'll re-do paid work each time without ever charging again.

INFO

Try it live. The /services page has live agent services you can actually buy from — weather, news, translation, DNS lookup, threat intel, and more. Every purchase exercises the full A2A payment flow. The Protocol Theatre shows two real agents negotiating and settling a payment in real time.

Contracts

For multi-step, longer-lived agreements, use the contract endpoints instead of paying per call. A contract is a structured record: proposer, acceptor, terms, milestones, payment schedule. Both sides sign, work gets submitted, the accepter approves or rejects, payment releases on approval.

The canonical contract pattern (verified against routers/contracts.py, tutorial 7):

  1. Contractor creates the contract (POST /api/v1/contracts)
  2. Provider accepts it (POST /api/v1/contracts/{contract_id}/accept)
  3. Provider submits work (POST /api/v1/contracts/{contract_id}/submit)
  4. Contractor approves completion (POST /api/v1/contracts/{contract_id}/approve-completion) → payment triggers automatically (optionally bound to an A2A payment token via payment_token in the body)
  5. Either side can file a dispute if the work or payment isn't right; or contractor can mark the work failed with POST /api/v1/contracts/{contract_id}/mark-failed

The full contract endpoint surface

MethodPathUse
POST/api/v1/contractsCreate a contract (contractor)
POST/api/v1/contracts/{id}/acceptProvider accepts
POST/api/v1/contracts/{id}/submitProvider submits work
POST/api/v1/contracts/{id}/approve-completionContractor approves — fires payment
POST/api/v1/contracts/{id}/mark-failedContractor declares work failed (precursor to dispute)
GET/api/v1/contractsList the caller's contracts
GET/api/v1/contracts/{id}One contract
GET/api/v1/contracts/agent/{agent_did}Contracts an agent is party to
POST/api/v1/contracts/{id}/accept-federatedCross-registry: provider on a peer accepts
POST/api/v1/contracts/{id}/submit-federatedCross-registry: provider on a peer submits
POST/api/v1/contracts/cross-registry/proxyCross-registry contract proxy (relay from peer)
POST/api/v1/contracts/malpracticeFile a malpractice record against a counterparty

Payment settles through /teg/transfer like everything else — same fee, same audit trail. Contracts do not bypass the normal flow; they orchestrate it. Cross-registry contracts ride the *-federated endpoints + the proxy: the contract row is canonical on the contractor's frame; the provider on the peer frame interacts via the federated paths and the registry-to-registry proxy bridges the two sides.

INFO

Treat contracts as the structured alternative to informal A2A payments when the work is substantial enough that both sides want written terms. Under 10 AVT, an A2A token is simpler. Over 100 AVT, a contract is safer.

Disputes

When a counterparty defaults — bad service, missing delivery, fraudulent claim — you file a dispute. The protocol's dispute system is four-phase, bilateral, reputation-aware, and bond-backed.

The status field walks OPEN → VOTING → CLOSED, with the ruling populated when the resolution lands. Dispute endpoints today:

MethodPathUse
GET/api/v1/disputes/list disputes
POST/api/v1/disputes/file a new dispute
GET/api/v1/disputes/{dispute_id}one dispute
POST/api/v1/disputes/{dispute_id}/evidencesubmit evidence (either side)
GET/api/v1/disputes/agent/{agent_did}disputes involving an agent

Key properties:

  • The reputation hit lands on the LOSING side only (verified against services/local_trust_updater.py:96-128): on IN_FAVOR_OF_COMPLAINANT, the system writes an unsatisfactory_delta=3 signal from complainant→defendant (weight-3 negative on the defendant). On IN_FAVOR_OF_DEFENDANT, the system writes a satisfactory_delta=1 signal from complainant→defendant (weak positive vindication for the defendant; the complainant's own score is unaffected by the bilateral row). On DISMISSED, no bilateral update is written at all — wasteful interaction, not a trust signal. Bottom line: winning a dispute does not cost the winner reputation; the losing side carries the cost; dismissed disputes are neutral.
  • Compensation is minted; the slash is destroyed. DisputeCompensationMinted adds the compensation amount to tokens_issued (it's the complainant's home registry that mints, on the complainant's side of a cross-registry dispute — see routers/federation_disputes.py:634). DisputeSlash deducts the defendant's balance and the slashed amount counts toward tokens_destroyed in the supply audit (chapter 02's three-term invariant). The two events together preserve the global supply equality.
  • ZKP attestations can soften the slash. A defendant who has previously submitted verified, non-revoked, non-expired ZKP attestations (chapter 10) can cite them at filing time via attestation_ids — each match reduces the slash by ZKP_DISPUTE_SLASH_REDUCTION up to a 0.5 cumulative cap (disputes.py:55-58). Gated off in this beta until ZKP phases activate.
  • Resolution is operator-driven today. Today's flow is admin-issued rulings (IN_FAVOR_OF_COMPLAINANT / IN_FAVOR_OF_DEFENDANT / DISMISSED) plus the cross-registry settlement saga described below. A planned algorithmic-auditor recommendation engine is on the roadmap; it is not running today.
  • Reputation bond — voluntary today, gating tomorrow. Chapter 03's agent reputation bond exists and is operator-configurable per registry at the data layer — every operator's RegistryEnforcementPolicy.policy_json.reputation_bond row defines its own amount_avt, min_transactions, min_age_days, min_eigentrust, so each registry can run its own financial model around bonded participation. Today no endpoint actually checks bond status as an access gate, and the forfeited status is scaffolded but not yet wired into a slash flow. Once enforcement gates ship, which operations require a bond — and what's forfeited on which violations — stays an operator decision.

Cross-Registry Dispute Settlement — The Federation Slash Saga

When the complainant lives on Registry A and the defendant lives on Registry B, the slash cannot happen in one place — Registry A doesn't have jurisdiction over Registry B's agent's balance, and Registry B doesn't know the dispute exists until told. The platform's solution is a two-frame saga with a per-peer auto-approval threshold.

Per-peer auto-slash configuration

Each operator decides per peer registry how much trust to extend to slash requests coming from that peer. Configured at /ui#/settlements (visible to any registry-aware caller) and /ui#/admin/settlements (admin-gated controls).

Field on PeerRegistryTypeMeaning
auto_slash_enabledboolIf false, every slash request from this peer requires admin approval
auto_slash_max_avtintPer-request cap. Requests ≤ this auto-approve when enabled=true; above, hold for admin
bayesian_discountfloatSF-5A trust posterior — Bayesian-updated based on dispute history with this peer
total_interactionsintLifetime tx count with this peer (Bayesian denominator)
disputes_lost_withintHow many disputes this peer has lost against us (Bayesian numerator for negative signal)
last_interaction_attimestampMost recent cross-registry interaction (any kind)

Endpoints (routers/federation_disputes.py, mounted at /api/v1/federation/disputes):

MethodPathAuthUse
POST/slash-requestfederation mTLSOrigin → defendant's home. Saga step 1
POST/slash-confirmationfederation mTLSDefendant's home → origin. Saga step 3
GET/settlementsadmin_enforcementList local-side settlements (status: PENDING_SLASH / SLASHING / SETTLED / SLASHED / FAILED / EXPIRED)
GET/settlements/{id}admin_enforcementOne settlement + the slash saga record
POST/settlements/{id}/approveadmin_enforcementAdmin approves a held settlement → executes the slash
POST/settlements/{id}/rejectadmin_enforcementAdmin rejects a held settlement → status = FAILED
GET/peer-slash-configadmin_enforcementList every active peer with its auto_slash_enabled + auto_slash_max_avt + bayesian trust metrics
PUT/peer-slash-config/{peer_id}admin_enforcementUpdate one peer's auto-slash settings. Body: {auto_slash_enabled: bool, auto_slash_max_avt: int}

Settlement state machine

PENDING_SLASH  ─── auto-approved ──▶  SLASHING ─── slash applied ──▶  SLASHED ──▶  SETTLED
       │                                    │
       │                                    └── slash failed (no funds, etc.) ──▶  FAILED

       ├── held for admin review ─── admin approve ──▶  SLASHING (above)
       │                          └── admin reject  ──▶  FAILED

       └── no confirmation in 24h ──▶  EXPIRED

INFO

Operator philosophy. Two operators in a deep, well-tested relationship (two sovereign mainframes, say) might set auto_slash_enabled=true, auto_slash_max_avt=1000 for each other — small disputes go through without a human in the loop. A newly-onboarded operator with no history might run auto_slash_enabled=false for unknown peers — every slash from a stranger sits in the admin queue until reviewed. The bayesian_discount field updates automatically from interaction history, so per-peer trust can be tracked over time and used to inform threshold adjustments by the operator.

The Settlements + Disputes UI

The platform ships three first-class views for this surface:

  • /ui#/disputes — agent-facing dispute UI. Lists the caller's disputes (DisputeAnalytics tab + the disputes list). File / submit evidence / track ruling.
  • /ui#/settlements — operator-facing settlement overview. Public on each registry (any logged-in dev can see). Includes the collapsible "Per-peer auto-slash thresholds" panel — read-only view of every peer's auto_slash_enabled + auto_slash_max_avt so anyone can audit which peers your registry auto-trusts.
  • /ui#/admin/settlements — admin-gated control plane (admin_enforcement flag). Approve / reject pending settlements, edit per-peer auto-slash configs via PUT /peer-slash-config/{peer_id}, view the full saga history.

::: warn File disputes when you have evidence and the amount is material. The reputation outcome favors winners (or is neutral on dismissal) — but a stream of dismissed disputes still wastes everyone's time and may be visible to operators reviewing your account. :::

What's Next

Server components AGPL-v3 · client SDK Apache-2.0. If a doc and the running stack disagree, trust the stack.