Machines That Pay: Implementing Google's A2A Protocol on a Live Financial Rail
Every payment flow we have today (OAuth challenges, 3D Secure redirects, bank KYC selfies) is a friction mechanism designed to prove a human is present. An AI agent has no browser, no phone, no face. It runs in a server loop at 3 AM and cannot be challenged.
Google's Agent2Agent (A2A) protocol and Agent Payments Protocol (AP2) are the specifications that define how an autonomous agent discovers, negotiates with, and pays another service without a human in the loop. This past weekend, we implemented a live, production-grade A2A settlement layer on PayArk's existing rails in Nepal.
This is an account of what it actually means to build it: the architectural decisions, the non-obvious parts, and what we still haven't solved.
The Problem with Human-Shaped Money
Build an agentic purchasing system on top of a human-shaped payment stack and you have three choices, all bad:
- Store raw credentials on the agent and let it authenticate as a human. You have now created an unconstrained credential with full account access. This is how companies get drained.
- Pre-authorize a fixed budget and let the agent burn it down. No per-vendor controls, no audit trail, no ability to revoke mid-run.
- Build a bespoke token system for every integration. This is what most teams do. It scales to exactly zero vendors before becoming unmaintainable.
AP2 mandates solve this with a clean primitive: bounded authority, cryptographically proven.
The Mandate: A Contract That Cannot Be Modified After Signing
The central data structure in AP2 is the Intent Mandate. A human principal (your CFO, an authorized software policy engine) signs a credential using their private key. That credential says, in verifiable tamper-proof language:
The bearer of this credential is authorized to spend up to NPR 5,000 at
vendor:payark-demo-storebefore2026-03-10T00:00:00Z. Any other action is explicitly unauthorized.
This is a W3C Verifiable Credential. Once signed, the authority is mathematically defined. No runtime flag, no database row, no admin toggle can override it. Our gateway does not decide if the agent is allowed to spend. It verifies whether the credential in front of it authorizes the specific transaction being requested.
Verification, not authorization. That distinction is the entire insight of the protocol.
Before A2A: AGENT -> "please authorize transaction X" -> SYSTEM decides yes/no
After A2A: AGENT -> "here is cryptographic proof I'm authorized for X" -> SYSTEM verifies proof
The system has no policy. The credential is the policy.
Anatomy of a Mandate
A mandate, as issued to our gateway:
{
"id": "mandate_01JN8K4Z9QWMXFCE7V2PRTB5D",
"type": "IntentMandate",
"issuer": "did:payark:principal:usr_01JN7Y3",
"subject": "did:payark:agent:agt_01JN8K2",
"issuanceDate": "2026-03-09T10:00:00Z",
"expirationDate": "2026-03-10T00:00:00Z",
"credentialSubject": {
"max_amount": 500000,
"currency": "NPR",
"currency_unit": "paisa",
"permitted_vendors": ["payark-demo-store"],
"permitted_actions": ["purchase"],
"idempotency_key": "session_k3p9x2m"
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2026-03-09T10:00:00Z",
"verificationMethod": "did:payark:principal:usr_01JN7Y3#keys-1",
"proofValue": "z58DAdFfa9A..."
}
}
A few things worth noting:
max_amount is in paisa, not rupees. Minor currency units remove floating-point ambiguity from financial math entirely.
permitted_vendors is a whitelist, not a category. An agent authorized to pay payark-demo-store cannot pay payark-live-store even if both are owned by the same entity.
idempotency_key is baked into the credential itself, not just the API call. If the agent retries a request, duplicate mandate presentations produce identical, idempotent results. The agent never double-charges you on a network timeout.
The Cart Mandate: Proof of Settlement
When a mandate is consumed by a successful transaction, our system generates a Cart Mandate: a new signed Verifiable Credential that serves as an immutable receipt. It is unforgeable proof of what was purchased, by which agent, for how much, at what timestamp. This is what makes agentic commerce non-repudiable.
The Protocol Flow
The A2A handshake between a Buyer Agent and our PayArk Settlement Agent follows a deterministic four-phase protocol. There is exactly one path to a successful settlement.
Notice what's absent: no human redirect, no OAuth dance, no popup, no SMS. The agent navigates the entire protocol. The only human involvement is the initial signing of the Intent Mandate, which can itself be a governance policy engine rather than a person.
Building the Verification Pipeline in Effect-TS
The verification pipeline is the most security-critical code we have shipped. A bug here does not produce a bad user experience. It produces financial loss.
We chose Effect-TS because it forces correctness through typed error channels. Every function in Effect declares exactly what it can fail with. The type system refuses to compile unless every failure case is handled.
// Standard async/await: errors are invisible at the type level
async function verifyMandate(mandateId: string) {
const mandate = await db.fetch(mandateId); // Can throw. Type says nothing.
if (mandate.expires_at < new Date()) {
throw new Error("expired"); // Caller has no idea this is possible.
}
return mandate;
}
// Effect: errors are part of the contract
const verifyMandate = (mandateId: string) =>
Effect.gen(function* () {
const mandate = yield* MandateRepository.findById(mandateId);
// Return type: Effect<Mandate, MandateNotFoundError | MandateExpiredError>
// The compiler rejects any caller that ignores these error cases.
});
The full verification pipeline runs four sequential checks. Each is a typed error boundary. If any fails, the pipeline short-circuits and returns a machine-readable error payload, not a generic 500.
export const verifyIntentMandate = (
mandateId: string,
amount: number, // always in paisa, never rupees
currency: "NPR",
vendorId: string,
idempotencyKey: string,
) =>
Effect.gen(function* () {
const db = yield* DbService;
const mandate = yield* db.mandates.findById(mandateId);
// Boundary 1: Temporal Envelope
// We use server time, not client-provided time.
if (new Date(mandate.expires_at) < new Date()) {
return yield* Effect.fail(
new MandateExpiredError({
mandateId,
expiredAt: mandate.expires_at,
attemptedAt: new Date().toISOString(),
}),
);
}
// Boundary 2: Fiscal Ceiling (cumulative, not per-transaction)
// An agent cannot make 10 x 499 paisa payments under a 500 paisa mandate.
const priorSpend = yield* db.mandates.getTotalSpend(mandateId);
if (priorSpend + amount > mandate.max_amount) {
return yield* Effect.fail(
new MandateViolationError({
mandateId,
authorized: mandate.max_amount,
requested: priorSpend + amount,
}),
);
}
// Boundary 3: Vendor Whitelist
if (!mandate.permitted_vendors.includes(vendorId)) {
return yield* Effect.fail(
new VendorNotPermittedError({
mandateId,
attemptedVendor: vendorId,
permittedVendors: mandate.permitted_vendors,
}),
);
}
// Boundary 4: Idempotency Guard
// If we have seen this key before, return the original result.
// Never execute a second settlement on a retry.
const existing = yield* db.settlements.findByIdempotencyKey(idempotencyKey);
if (existing) {
return yield* Effect.fail(
new DuplicateMandateError({
idempotencyKey,
existingSettlementId: existing.id,
}),
);
}
return mandate;
});
Two things here that are not obvious but matter in production:
Cumulative spend tracking. Boundary 2 does not check if this transaction alone exceeds the limit. It queries the total amount already spent under this mandate. Without this, an agent can loop a request 100 times with small amounts and exceed an authorized budget without ever triggering the individual-transaction check.
Idempotency in the credential. The idempotency_key was baked into the original signed VC. Boundary 4 checks a database of settled transactions. If the key exists, we return the original settlement record rather than executing again. This is the behavior agents need when running in retry loops.
The Mandate Lifecycle
A mandate's state transitions are strict and linear. No cycles. No backward transitions. Understanding this is essential for building correct tooling.
Three terminal states exist, and none can be reversed. This is intentional. Immutable state machines are auditable. Mutable ones are attack surfaces.
When a mandate lands in Violated, we write a structured audit event:
{
"event_type": "MANDATE_VIOLATED",
"mandate_id": "mandate_01JN8K4Z9Q",
"violation_type": "FISCAL_CEILING_EXCEEDED",
"authorized_amount": 500000,
"requested_amount": 500100,
"agent_id": "agt_01JN8K2",
"timestamp": "2026-03-09T14:32:11.042Z",
"trace_id": "01JN8K9ZQMXF"
}
This event is routed to the principal's notification channel. The agent gets a structured 403 with a machine-readable body so it can reason about why it failed and whether to retry with a different mandate.
Discovery: /.well-known/agent.json
Before any mandate exchange, a Buyer Agent must discover our capabilities. A2A mandates a /.well-known/agent.json endpoint: a machine-readable capability manifest, equivalent to OpenAPI but for agentic identity.
// apps/api/src/handlers/discovery.ts
export const discoveryHandler = HttpApiHandler.make(
DiscoveryApi,
({ send }) => ({
"GET /agent.json": send(200, {
schema_version: "a2a-v1",
agent_id: "did:payark:settlement:gateway-001",
name: "PayArk Settlement Agent",
capabilities: {
protocols: ["AP2-v1", "A2A-v1"],
payment_rails: ["eSewa", "Khalti", "ConnectIPS"],
supported_currencies: ["NPR"],
mandate_types: ["IntentMandate"],
receipt_types: ["CartMandate"],
idempotency: true,
cumulative_spend_tracking: true,
},
endpoints: {
mandate_intent: "/v1/mandates/intent",
checkout: "/v1/checkout",
mandate_status: "/v1/mandates/{mandate_id}",
},
trust: {
did_document: "https://api.payark.io/.well-known/did.json",
verification_method: "Ed25519Signature2020",
},
}),
}),
);
When a Buyer Agent GETs this endpoint, it learns which payment rails are available, which mandate types are accepted, and how to verify our identity via our DID document. This is Content-Type negotiation, but for financial agency.
Testing the Protocol Under Adversarial Conditions
We don't trust the code. We prove it. Our test suite treats the verification pipeline as a state machine and exhaustively covers every edge.
describe("verifyIntentMandate: adversarial suite", () => {
it("rejects a mandate that expired 1ms ago", async () => {
const mandate = buildMandate({
expires_at: new Date(Date.now() - 1).toISOString(),
});
const result = await Effect.runPromiseExit(
verifyIntentMandate(mandate.id, 100, "NPR", "vendor-x", "key-1"),
);
expect(Exit.isFailure(result)).toBe(true);
expect(Cause.failureOption(result.cause)).toMatchObject({
_tag: "MandateExpiredError",
});
});
it("rejects a request that cumulatively exceeds the limit", async () => {
// Prior spend: 4,900 paisa. Limit: 5,000. Request: 200. Total: 5,100. Should fail.
const mandate = buildMandate({ max_amount: 5000 });
await seedPriorSpend(mandate.id, 4900);
const result = await Effect.runPromiseExit(
verifyIntentMandate(mandate.id, 200, "NPR", "permitted-vendor", "key-2"),
);
expect(Exit.isFailure(result)).toBe(true);
expect(Cause.failureOption(result.cause)).toMatchObject({
_tag: "MandateViolationError",
authorized: 5000,
requested: 5100,
});
});
it("rejects payment to a vendor not in the permitted list", async () => {
const mandate = buildMandate({ permitted_vendors: ["vendor-a"] });
const result = await Effect.runPromiseExit(
verifyIntentMandate(mandate.id, 100, "NPR", "vendor-b", "key-3"),
);
expect(Exit.isFailure(result)).toBe(true);
expect(Cause.failureOption(result.cause)._tag).toBe(
"VendorNotPermittedError",
);
});
it("returns the original result for a duplicate idempotency key", async () => {
const mandate = buildMandate({});
await seedSettlement({
mandate_id: mandate.id,
idempotency_key: "dup-key",
});
const result = await Effect.runPromiseExit(
verifyIntentMandate(mandate.id, 100, "NPR", "vendor-a", "dup-key"),
);
expect(Exit.isFailure(result)).toBe(true);
expect(Cause.failureOption(result.cause)._tag).toBe(
"DuplicateMandateError",
);
});
});
The suite runs against a structural mock (no database, no network). Edge cases that are impossible to trigger in staging, like "mandate expired exactly 1ms ago," are provably handled.
What Nepal Specifically Makes Interesting
Nepal sits at a regulatory inflection point. NRB Directive 2081/82 mandates that all government and public entities transition to 100% digital revenue collection by mid-2025. That directive applies to humans. The systems those humans build are increasingly agentic.
ConnectIPS supports direct debit collection. eSewa and Khalti dominate consumer wallets. There is now a settlement layer that speaks both languages: the legacy push-payment semantics of NRB-compliant carriers, and the cryptographic intent semantics of A2A. PayArk is the translation layer between two monetary eras.
The A2A specification was finalized weeks ago. PayArk had a production implementation on Nepali rails days later. No waiting for a larger market to validate the technology first. No six-month procurement cycle. For the first time, Nepal shipped alongside San Francisco.
What We Have Not Solved
DID resolution latency. Our current implementation resolves issuer DID documents by fetching from a known registry. At scale, this adds 100-200ms to every mandate verification. We are building a cache with a TTL tied to the credential's expirationDate.
Principal key rotation. If a human principal rotates their signing key mid-mandate-lifecycle, mandates signed with the old key are invalid under strict VC semantics. We need a grace period policy and a re-signing flow for long-lived mandates.
Multi-principal mandates. The current spec supports a single issuer per mandate. Enterprise purchasing frequently requires dual-signature authorization: finance plus engineering approval for infrastructure spend above a threshold. This is not in the AP2 spec yet. We are watching the working group.
The Road Ahead
The agentic economy is not a prediction. The protocols exist. The cryptographic primitives exist. What did not exist until now was a compliant financial settlement layer in South Asia that speaks the language of autonomous agents.
Bounded authority. Verifiable credentials. Immutable receipts. These are the foundational primitives for a monetary system that can survive a world where most transactions are initiated by machines.
Nepal is not behind on this one.
Himansu Rawal is the founder of PayArk.