See the 402 become a 200.
Create an ephemeral fulfillment endpoint, take it into your own VS Code project, or inspect the same bound-v2 402 flow with the hosted controls. Every delivered resource has an exact-request agent signature and a verified testnet payment; the fourth purchase is rejected by the contract.
Fresh testnet-only identities · no wallet connection · no secrets shown or stored in this page
PAYMENT WORKBENCH
not createdThe server creates and funds the disposable testnet actors only when you start.
Request lifecycle
402 → contract → proof → 200- HTTP 402
- Contract
- Proof
- HTTP 200
Implementation
import { DeliveryPendingError, getSettlementReceipt, reapp } from "@reapp-sdk/core";
import { Keypair } from "@stellar/stellar-sdk";
import { randomUUID } from "node:crypto";
import { chmod, mkdir, open, readFile, rename, rm } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const endpointBase = "https://your-endpoint.example/api/express/session/source";
const merchant = "G...MERCHANT";
// Add .reapp/ to .gitignore. It contains sensitive payment-proof recovery state.
const receiptFile = resolve(".reapp/pending-receipts.json");
const resultFile = resolve(".reapp/accepted-results.json");
let pendingReceipts = {};
let acceptedResults = {};
try {
pendingReceipts = JSON.parse(await readFile(receiptFile, "utf8"));
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
try {
acceptedResults = JSON.parse(await readFile(resultFile, "utf8"));
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
if (Object.keys(pendingReceipts).length > 0 || Object.keys(acceptedResults).length > 0) {
throw new Error("Prior payment evidence exists. Refusing to create new keys or pay again; recover the exact retained state first.");
}
const user = Keypair.random();
const agentKey = Keypair.random();
let writeQueue = Promise.resolve();
async function writeDurableJson(filePath, value) {
const snapshot = JSON.stringify(value, null, 2) + "\n";
const operation = writeQueue.then(async () => {
const directory = dirname(filePath);
await mkdir(directory, { recursive: true, mode: 0o700 });
await chmod(directory, 0o700);
const temporary = filePath + "." + process.pid + "." + randomUUID() + ".tmp";
let handle;
try {
handle = await open(temporary, "wx", 0o600);
await handle.writeFile(snapshot, "utf8");
await handle.sync();
await handle.close();
handle = undefined;
await rename(temporary, filePath);
await chmod(filePath, 0o600);
const directoryHandle = await open(directory, "r");
try { await directoryHandle.sync(); } finally { await directoryHandle.close(); }
} finally {
await handle?.close().catch(() => undefined);
await rm(temporary, { force: true }).catch(() => undefined);
}
});
writeQueue = operation.catch(() => undefined);
await operation;
}
async function persistReceipts() {
await writeDurableJson(receiptFile, pendingReceipts);
}
const receiptStore = {
async savePending(receipt) {
pendingReceipts[receipt.receiptId] = receipt;
await persistReceipts();
},
async clearPending(receiptId) {
delete pendingReceipts[receiptId];
await persistReceipts();
},
async listPending() {
return Object.values(pendingReceipts);
},
};
async function fund(keypair) {
const response = await fetch(
"https://friendbot.stellar.org/?addr=" + keypair.publicKey(),
);
if (!response.ok) throw new Error("Friendbot funding failed");
}
await Promise.all([fund(user), fund(agentKey)]);
await new Promise((resolve) => setTimeout(resolve, 3000));
const mandate = reapp.createIntentMandate({
user: user.publicKey(),
agent: agentKey.publicKey(),
merchant,
asset: reapp.testnet.nativeSac,
maxAmount: "3.00",
expiry: Math.floor(Date.now() / 1000) + 3600,
});
const registerTx = await reapp.registerMandate(mandate, { signer: user });
const approveTx = await reapp.approveBudget(mandate, { signer: user });
console.log("mandate ready", { registerTx, approveTx });
const agent = reapp.agent({
mandate,
signer: agentKey,
proofPolicy: "bound-v2-only",
receiptStore,
});
const resources = ["market", "academic", "news", "patents"];
const delivered = [];
let rejected = 0;
for (const [index, resource] of resources.entries()) {
try {
let response;
try {
response = await agent.fetch(endpointBase + "/" + resource);
} catch (error) {
if (!(error instanceof DeliveryPendingError)) throw error;
console.log("broadcast may have been attempted; retrying the exact receipt", error.receipt.txHash);
response = await agent.retryDelivery(error.receipt);
}
const body = await response.json();
if (index === 3) throw new Error("The fourth request unexpectedly succeeded");
if (
response.status !== 200
|| body.ok !== true
|| typeof body.settledTx !== "string"
|| typeof body.data !== "string"
) {
throw new Error(resource + " returned an invalid protected response");
}
const receipt = getSettlementReceipt(response);
if (
!receipt
|| receipt.proofVersion !== 2
|| body.settledTx.toLowerCase() !== receipt.txHash.toLowerCase()
) throw new Error("Missing or mismatched bound-v2 receipt");
acceptedResults[resource] = {
url: endpointBase + "/" + resource,
txHash: body.settledTx,
data: body.data,
};
await writeDurableJson(resultFile, acceptedResults);
await agent.acknowledgeDelivery(receipt);
delivered.push(body.settledTx);
console.log("delivered", { resource, status: response.status, settledTx: body.settledTx });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (index !== 3 || !/(?:Contract,\s*#6|BudgetExceeded)/.test(message)) throw error;
const report = await fetch(endpointBase + "/" + resource, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ event: "contract_rejected", mandateId: mandate.id }),
});
if (!report.ok) {
throw new Error("Final on-chain budget verification failed with HTTP " + report.status);
}
rejected = 1;
console.log("contract rejected", { resource, message });
}
}
if (delivered.length !== 3 || rejected !== 1 || new Set(delivered).size !== 3) {
throw new Error("Expected three unique deliveries and one contract rejection");
}
console.log("REAPP TESTNET FLOW PASSED", {
delivered: 3,
rejected: 1,
uniqueSettlements: 3,
});Raw request / response
Create a workspace, then stage the unpaid and paid requests here.
Live transaction evidence
Stellar testnet · opens in stellar.expertCreate a workspace to see the authorization and payment transactions.
Run both reference agents
npm run agents:testnetOne command creates fresh testnet actors, registers the 3 XLM mandate, starts the 402-gated Express API, serves three paid resources, and proves the fourth purchase cannot exceed the budget.
Consumer agent
import { DeliveryPendingError, getSettlementReceipt, reapp } from "@reapp-sdk/core";
import { Keypair } from "@stellar/stellar-sdk";
import { randomUUID } from "node:crypto";
import { chmod, mkdir, open, readFile, rename, rm } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const endpointBase = "https://your-endpoint.example/api/express/session/source";
const merchant = "G...MERCHANT";
// Add .reapp/ to .gitignore. It contains sensitive payment-proof recovery state.
const receiptFile = resolve(".reapp/pending-receipts.json");
const resultFile = resolve(".reapp/accepted-results.json");
let pendingReceipts = {};
let acceptedResults = {};
try {
pendingReceipts = JSON.parse(await readFile(receiptFile, "utf8"));
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
try {
acceptedResults = JSON.parse(await readFile(resultFile, "utf8"));
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
if (Object.keys(pendingReceipts).length > 0 || Object.keys(acceptedResults).length > 0) {
throw new Error("Prior payment evidence exists. Refusing to create new keys or pay again; recover the exact retained state first.");
}
const user = Keypair.random();
const agentKey = Keypair.random();
let writeQueue = Promise.resolve();
async function writeDurableJson(filePath, value) {
const snapshot = JSON.stringify(value, null, 2) + "\n";
const operation = writeQueue.then(async () => {
const directory = dirname(filePath);
await mkdir(directory, { recursive: true, mode: 0o700 });
await chmod(directory, 0o700);
const temporary = filePath + "." + process.pid + "." + randomUUID() + ".tmp";
let handle;
try {
handle = await open(temporary, "wx", 0o600);
await handle.writeFile(snapshot, "utf8");
await handle.sync();
await handle.close();
handle = undefined;
await rename(temporary, filePath);
await chmod(filePath, 0o600);
const directoryHandle = await open(directory, "r");
try { await directoryHandle.sync(); } finally { await directoryHandle.close(); }
} finally {
await handle?.close().catch(() => undefined);
await rm(temporary, { force: true }).catch(() => undefined);
}
});
writeQueue = operation.catch(() => undefined);
await operation;
}
async function persistReceipts() {
await writeDurableJson(receiptFile, pendingReceipts);
}
const receiptStore = {
async savePending(receipt) {
pendingReceipts[receipt.receiptId] = receipt;
await persistReceipts();
},
async clearPending(receiptId) {
delete pendingReceipts[receiptId];
await persistReceipts();
},
async listPending() {
return Object.values(pendingReceipts);
},
};
async function fund(keypair) {
const response = await fetch(
"https://friendbot.stellar.org/?addr=" + keypair.publicKey(),
);
if (!response.ok) throw new Error("Friendbot funding failed");
}
await Promise.all([fund(user), fund(agentKey)]);
await new Promise((resolve) => setTimeout(resolve, 3000));
const mandate = reapp.createIntentMandate({
user: user.publicKey(),
agent: agentKey.publicKey(),
merchant,
asset: reapp.testnet.nativeSac,
maxAmount: "3.00",
expiry: Math.floor(Date.now() / 1000) + 3600,
});
const registerTx = await reapp.registerMandate(mandate, { signer: user });
const approveTx = await reapp.approveBudget(mandate, { signer: user });
console.log("mandate ready", { registerTx, approveTx });
const agent = reapp.agent({
mandate,
signer: agentKey,
proofPolicy: "bound-v2-only",
receiptStore,
});
const resources = ["market", "academic", "news", "patents"];
const delivered = [];
let rejected = 0;
for (const [index, resource] of resources.entries()) {
try {
let response;
try {
response = await agent.fetch(endpointBase + "/" + resource);
} catch (error) {
if (!(error instanceof DeliveryPendingError)) throw error;
console.log("broadcast may have been attempted; retrying the exact receipt", error.receipt.txHash);
response = await agent.retryDelivery(error.receipt);
}
const body = await response.json();
if (index === 3) throw new Error("The fourth request unexpectedly succeeded");
if (
response.status !== 200
|| body.ok !== true
|| typeof body.settledTx !== "string"
|| typeof body.data !== "string"
) {
throw new Error(resource + " returned an invalid protected response");
}
const receipt = getSettlementReceipt(response);
if (
!receipt
|| receipt.proofVersion !== 2
|| body.settledTx.toLowerCase() !== receipt.txHash.toLowerCase()
) throw new Error("Missing or mismatched bound-v2 receipt");
acceptedResults[resource] = {
url: endpointBase + "/" + resource,
txHash: body.settledTx,
data: body.data,
};
await writeDurableJson(resultFile, acceptedResults);
await agent.acknowledgeDelivery(receipt);
delivered.push(body.settledTx);
console.log("delivered", { resource, status: response.status, settledTx: body.settledTx });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (index !== 3 || !/(?:Contract,\s*#6|BudgetExceeded)/.test(message)) throw error;
const report = await fetch(endpointBase + "/" + resource, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ event: "contract_rejected", mandateId: mandate.id }),
});
if (!report.ok) {
throw new Error("Final on-chain budget verification failed with HTTP " + report.status);
}
rejected = 1;
console.log("contract rejected", { resource, message });
}
}
if (delivered.length !== 3 || rejected !== 1 || new Set(delivered).size !== 3) {
throw new Error("Expected three unique deliveries and one contract rejection");
}
console.log("REAPP TESTNET FLOW PASSED", {
delivered: 3,
rejected: 1,
uniqueSettlements: 3,
});Use this one-shot example in a clean VS Code project, where your user and agent keys stay under your control. It fails closed on retained evidence instead of silently creating new keys; the repository reference agent demonstrates full same-identity restart recovery. The consumer never transfers tokens directly or treats cached budget state as authority; the contract decides whether every spend is allowed.
Fulfillment agent
import {
InMemoryBoundRedemptionStore,
createBoundReappPaidJsonRoute,
} from "@reapp-sdk/express-middleware";
const challengeSecret = process.env.REAPP_CHALLENGE_SECRET;
if (!challengeSecret) throw new Error("REAPP_CHALLENGE_SECRET is required");
// Demo only. Use a durable, shared BoundRedemptionStore in production.
const redemptionStore = new InMemoryBoundRedemptionStore();
const paidSource = createBoundReappPaidJsonRoute({
merchant,
sourceAccount: merchant,
audience: "https://api.example", // exact public origin, never Host-derived
challengeSecret,
redemptionStore,
amount: "1.00",
resource: (request) => request.originalUrl,
}, async ({ request, payment }) => ({
body: {
ok: true,
resource: request.params.id,
settledTx: payment.txHash,
data: "protected value",
},
}));
app.get("/source/:id", paidSource);The paid JSON route authenticates the challenge, binds the exact origin and GET resource, checks transaction freshness, the MandateRegistry event and matching SEP-41 transfer, then verifies the chain-derived mandate agent signature. One proof atomically claims one callback execution and stores exact JSON bytes; completed recovery replays those bytes without rerunning work. The interactive demo uses a workspace-scoped memory store; production requires a durable shared claim/result store.
Request lifecycle
HTTP 402The API authenticates an exact-origin GET challengeContractexecute_payment re-checks and consumes the mandateProofThe chain-derived mandate agent signs that request and transactionHTTP 200Only a verified payment unlocks the resourceKeep the safe boundary
- Never trust the amount, merchant, agent, or mandate claimed in an HTTP header.
- Never serve before independent chain verification and exact-request signature verification.
- Never replace execute_payment with a direct token transfer or cached application check.