// =============================================================================
// STRIPE AGENT (AP2 Payment Processor)
// =============================================================================
// This is a MOCK Stripe agent that demonstrates AP2 mandate verification.
//
// In REAL WORLD: This would be Stripe's actual payment API.
// In this DEMO: We verify mandates and simulate payment processing.
//
// KEY POINT: Stripe REFUSES payment without a valid mandate.
// The mandate proves the user approved this specific payment.
// =============================================================================
import { createHmac } from 'crypto';
import { createServer } from 'http';
import type {
AgentCard,
JSONRPCRequest,
JSONRPCResponse,
Mandate,
PaymentResponse,
PaymentStatusResponse,
PaymentRequestWithMandate,
RefundResponse,
RefundRequestWithMandate,
} from '../../src/types.js';
// =============================================================================
// CONFIG
// =============================================================================
const REGISTRY_URL = `http://${process.env.REGISTRY_HOST || 'localhost'}:8004`;
// User's secret key - Stripe knows this to verify mandates
// In real world: This would be a public key verification, not shared secret
const USER_SECRET = process.env.USER_SECRET || 'user-secret-key';
// In-memory payment store (real Stripe has a database)
const payments = new Map<string, PaymentStatusResponse>();
// =============================================================================
// MANDATE VERIFICATION
// =============================================================================
// Stripe MUST verify the mandate before processing any payment.
// This is the core of AP2 security - no valid mandate = no payment.
function createMandatePayload(mandate: Omit<Mandate, 'signature'>): string {
return JSON.stringify({
userId: mandate.userId,
agentId: mandate.agentId,
action: mandate.action,
maxAmount: mandate.maxAmount,
currency: mandate.currency,
orderId: mandate.orderId,
issuedAt: mandate.issuedAt,
expiresAt: mandate.expiresAt,
});
}
function verifyMandateSignature(mandate: Mandate): boolean {
const payload = createMandatePayload(mandate);
const expectedSignature = createHmac('sha256', USER_SECRET).update(payload).digest('hex');
return mandate.signature === expectedSignature;
}
function verifyMandate(mandate: Mandate, requestedAmount?: number): { valid: boolean; reason?: string } {
// Check 1: Signature valid? (proves user signed it)
if (!verifyMandateSignature(mandate)) {
return { valid: false, reason: 'Invalid signature - mandate tampered!' };
}
// Check 2: Not expired? (mandates are time-limited)
if (new Date() > new Date(mandate.expiresAt)) {
return { valid: false, reason: 'Mandate expired' };
}
// Check 3: Amount within limit? (can't charge more than approved)
if (requestedAmount !== undefined && requestedAmount > mandate.maxAmount) {
return { valid: false, reason: `Amount ${requestedAmount} exceeds limit ${mandate.maxAmount}` };
}
return { valid: true };
}
// =============================================================================
// STRIPE AGENT CLASS
// =============================================================================
class StripeAgent {
private agentId: string;
private name: string;
private port: number;
private httpServer: ReturnType<typeof createServer> | null = null;
constructor(agentId: string, name: string, port: number) {
this.agentId = agentId;
this.name = name;
this.port = port;
}
// ---------------------------------------------------------------------------
// START SERVER & REGISTER WITH REGISTRY
// ---------------------------------------------------------------------------
async start(): Promise<void> {
// Create HTTP server for JSON-RPC requests
this.httpServer = createServer(async (req, res) => {
if (req.method === 'POST') {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
const response = await this.handleRequest(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} else {
res.writeHead(404);
res.end();
}
});
this.httpServer.listen(this.port, '0.0.0.0', () => {
console.log(`💳 ${this.name} running on port ${this.port}`);
});
// Register with A2A registry so other agents can find us
const card: AgentCard = {
agentId: this.agentId,
name: this.name,
version: '1.0.0',
endpoint: `http://stripe-agent:${this.port}`,
capabilities: ['process_payment', 'check_payment_status', 'process_refund'],
description: 'Stripe Payment Agent (AP2 with Mandates)',
};
await fetch(`${REGISTRY_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(card),
});
console.log(`💳 ${this.name} registered with registry`);
}
// ---------------------------------------------------------------------------
// HANDLE JSON-RPC REQUEST
// ---------------------------------------------------------------------------
private async handleRequest(request: JSONRPCRequest): Promise<JSONRPCResponse> {
const { method, params, id } = request;
let result: unknown;
try {
switch (method) {
case 'payment/process':
result = await this.processPayment(params as unknown as PaymentRequestWithMandate);
break;
case 'payment/status':
result = await this.checkPaymentStatus((params as unknown as { transactionId: string }).transactionId);
break;
case 'payment/refund':
result = await this.processRefund(params as unknown as RefundRequestWithMandate);
break;
default:
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } };
}
return { jsonrpc: '2.0', id, result };
} catch (error) {
return { jsonrpc: '2.0', id, error: { code: -32000, message: error instanceof Error ? error.message : 'Unknown error' } };
}
}
// ---------------------------------------------------------------------------
// PROCESS PAYMENT (requires mandate)
// ---------------------------------------------------------------------------
private async processPayment(request: PaymentRequestWithMandate): Promise<PaymentResponse> {
// Step 1: Mandate required
if (!request.mandate) {
console.log('❌ REJECTED: No mandate');
throw new Error('Payment rejected: No mandate. AP2 requires authorization.');
}
// Step 2: Verify mandate
console.log('🔐 Verifying mandate...');
const verification = verifyMandate(request.mandate, request.amount);
if (!verification.valid) {
console.log(`❌ REJECTED: ${verification.reason}`);
throw new Error(`Payment rejected: ${verification.reason}`);
}
console.log('✅ Mandate verified');
// Step 3: Process payment (simulated)
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const timestamp = new Date().toISOString();
const success = Math.random() > 0.1; // 90% success rate
const status = success ? 'completed' : 'failed';
// Store payment for status checks
payments.set(transactionId, {
transactionId,
status,
amount: request.amount,
currency: request.currency,
timestamp,
paymentMethod: request.paymentMethod,
orderId: request.orderId,
});
console.log(`💳 Payment ${status}: ${transactionId}`);
return {
transactionId,
status,
amount: request.amount,
currency: request.currency,
timestamp,
message: success ? 'Payment processed via Stripe (mandate verified)' : 'Payment failed',
};
}
// ---------------------------------------------------------------------------
// CHECK PAYMENT STATUS (no mandate needed)
// ---------------------------------------------------------------------------
private async checkPaymentStatus(transactionId: string): Promise<PaymentStatusResponse> {
const payment = payments.get(transactionId);
if (!payment) throw new Error(`Payment not found: ${transactionId}`);
return payment;
}
// ---------------------------------------------------------------------------
// PROCESS REFUND (requires mandate)
// ---------------------------------------------------------------------------
private async processRefund(request: RefundRequestWithMandate): Promise<RefundResponse> {
// Step 1: Mandate required for refunds too
if (!request.mandate) {
console.log('❌ REJECTED: No mandate for refund');
throw new Error('Refund rejected: No mandate.');
}
// Step 2: Verify mandate
console.log('🔐 Verifying refund mandate...');
const verification = verifyMandate(request.mandate, request.amount);
if (!verification.valid) {
console.log(`❌ REJECTED: ${verification.reason}`);
throw new Error(`Refund rejected: ${verification.reason}`);
}
console.log('✅ Refund mandate verified');
// Step 3: Find original payment
const payment = payments.get(request.transactionId);
if (!payment) throw new Error(`Payment not found: ${request.transactionId}`);
// Step 4: Process refund (simulated)
const refundAmount = request.amount || payment.amount;
const refundId = `refund-${Date.now()}-${Math.random().toString(36).slice(2)}`;
console.log(`💸 Refund processed: ${refundId}`);
return {
refundId,
transactionId: request.transactionId,
amount: refundAmount,
currency: payment.currency,
status: 'completed',
timestamp: new Date().toISOString(),
message: `Refund of ${refundAmount} ${payment.currency} processed via Stripe`,
};
}
stop(): void {
this.httpServer?.close();
}
}
// =============================================================================
// START AGENT
// =============================================================================
const stripeAgent = new StripeAgent('stripe-agent-001', 'Stripe Agent', 8005);
stripeAgent.start().catch((error) => {
console.error('Failed to start Stripe agent:', error);
process.exit(1);
});