Agent Patterns
Consumer agents initiate payments on behalf of users. Fulfillment agents receive payments for delivered services. Evaluator agents attest completion for deferred work. Three composable patterns covering the full agentic commerce surface.
Two Core Agent Types
REAPP defines two composable runtime patterns. Consumer agents hold a delegated mandate and autonomously execute payments to acquire services. Fulfillment agents expose services behind x402 paywalls and receive payments from consumer agents. Both patterns ship as reference implementations in the SDK.
Consumer Agent
import { createReappAgent, loadIntentMandate } from '@reapp/sdk';
import { createKeypairSigner } from '@x402/fetch';
const mandate = await loadIntentMandate(mandateId);
const agent = createReappAgent({
mandate,
signer: createKeypairSigner(agentKeypair), // agent key, NOT user key
mandateContract: MANDATE_CONTRACT_ADDRESS,
network: 'stellar:pubnet',
});
// Transparently handles full 7-step x402 flow
const data = await agent.fetch('https://api.dataservice.com/premium/report');Fulfillment Agent
import express from 'express';
import { reappMiddleware } from '@reapp/sdk/express';
const app = express();
app.use(reappMiddleware({
payTo: merchantStellarAddress,
facilitatorUrl: 'https://channels.openzeppelin.com/x402/testnet',
routes: {
'GET /api/data/*': { price: '0.10', asset: 'USDC' },
'POST /api/compute': { price: '1.00', asset: 'USDC' },
},
requireMandate: { 'POST /api/compute': true },
registry: MANDATE_CONTRACT_ADDRESS,
}));The Evaluator Agent — When It's Needed
x402 works perfectly when delivery and payment are coupled — agent pays, server instantly returns data. But deferred or qualitative work breaks this model. Translate a document, write code, design a logo — payment happens now, delivery happens later. How do you know the work was actually done and done correctly?
Three Evaluator Tiers
REAPP ships three evaluator implementations ordered by trust level and value. The appropriate tier is selected at job creation based on the task value and whether correctness is objectively verifiable.
Escrow + Evaluator Lifecycle
Money goes into Soroban escrow at payment time. The evaluator attests completion. Escrow releases to the provider on approval, refunds to the client on rejection or expiry. No funds move until the evaluator signs — and the evaluator's reputation score is updated on-chain based on dispute outcomes.
The escrow timeout window, dispute resolution flow, and evaluator collateral requirements shown here are testnet placeholders. Production parameters — including timeout durations, minimum collateral stakes, and the reputation update formula — require stress testing with real transaction volumes and adversarial conditions before mainnet deployment.
Evaluator Interface
interface ReappEvaluator {
// Called by fulfillment agent when work is submitted
submitWork(jobId: string, deliverableHash: string): Promise<void>;
// Called by evaluator to attest completion
attestCompletion(
jobId: string,
approved: boolean,
reason?: string
): Promise<TxHash>;
// On-chain: escrow releases to provider on approval,
// refunds to client on rejection or expiry
}Deterministic outputs: API responses, file downloads. Consumer agent verifies delivery itself.
Qualitative tasks. REAPP-maintained ADK agent uses LLM judge to assess quality vs specification.
Any third party — DAO, multisig, specialized AI. Evaluators stake collateral, build reputation on-chain.
The value thresholds shown (< $5 / $5–$100 / > $100), staking collateral amounts, reputation scoring formula, and slashing conditions are testnet defaults chosen for demo clarity. Production values will be determined through real-world evaluation of transaction patterns and adversarial testing. The tier boundaries, in particular, depend on observed agent behavior at scale — they may shift significantly once real payment volumes are measured.
Agent Framework — Google ADK TypeScript
The TypeScript ADK shipped in December 2025 at v0.4.0 — functional for prototyping but carrying a "Pre-GA" disclaimer. Python remains the mature path at v1.26.0. For REAPP's demo, ADK TypeScript is sufficient. The critical insight: FunctionTool.execute is arbitrary TypeScript — an x402 payment call fits naturally.
import {
LlmAgent, // Main LLM-powered agent
FunctionTool, // Custom function tool (Zod schema params)
InMemoryRunner, // In-memory runner for dev/testing
SequentialAgent, // Runs sub-agents in order
ParallelAgent, // Runs sub-agents concurrently
} from '@google/adk';
// x402 payment as an ADK tool
const makeX402Payment = new FunctionTool({
name: 'pay_for_resource',
description: 'Pay for API access via x402 on Stellar',
parameters: z.object({
resourceUrl: z.string(),
maxAmountUSD: z.number(),
}),
execute: async ({ resourceUrl, maxAmountUSD }) => {
const response = await x402Fetch(resourceUrl, { maxAmount: maxAmountUSD });
return { status: response.status, data: await response.json() };
},
});Multi-Agent Delegation
ADK's subAgents on LlmAgent enables automatic delegation via the LLM's transfer_to_agent mechanism:
const fulfillmentAgent = new LlmAgent({
name: 'fulfillment_agent',
model: 'gemini-2.5-flash',
description: 'Handles payments and order processing on Stellar',
tools: [makeX402Payment, checkBalance, getTransactionStatus],
});
const consumerAgent = new LlmAgent({
name: 'consumer_agent',
model: 'gemini-2.5-flash',
instruction: 'Help users shop. Delegate payment tasks to fulfillment_agent.',
subAgents: [fulfillmentAgent],
});Chat UI — Vercel AI SDK v6 + assistant-ui
For the consumer-facing demo, the optimal stack is Vercel AI SDK v6 (ai + @ai-sdk/react) for backend streaming and tool calling, paired with assistant-ui (@assistant-ui/react, 450K+ monthly npm downloads) for polished chat primitives. AI SDK v6 introduced ToolLoopAgent, parts-based message rendering for rich inline content, and human-in-the-loop tool approval.
// app/api/chat/route.ts — Server-side streaming with payment tools
import { streamText, convertToModelMessages } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: 'openai/gpt-4o',
messages: await convertToModelMessages(messages),
tools: {
sendPayment: {
description: 'Send USDC payment on Stellar',
parameters: z.object({
recipient: z.string(),
amount: z.number(),
memo: z.string().optional(),
}),
execute: async ({ recipient, amount }) => {
const tx = await stellarClient.transfer({
to: recipient, amount: BigInt(amount * 1e7),
});
return { txHash: tx.hash, status: 'confirmed', amount };
},
},
},
});
return result.toUIMessageStreamResponse();
}// Client-side — rendering payment confirmations inline
{message.parts.map((part, i) => {
switch (part.type) {
case 'text': return <p key={i}>{part.text}</p>;
case 'tool-sendPayment':
if (part.state === 'partial-call') return <PaymentPending key={i} />;
if (part.state === 'result') return <PaymentCard key={i} result={part.result} />;
}
})}