LIVE EXPRESS WORKBENCH · STELLAR TESTNET

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 created

The server creates and funds the disposable testnet actors only when you start.

0
served
0
blocked
1.00 XLM
price
Mandate budget
0.00 XLM of 3.00 XLM
3.00 XLM remaining

Request lifecycle

402 → contract → proof → 200
  1. HTTP 402
  2. Contract
  3. Proof
  4. 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,
});
Run this one-shot example in a clean project. It fails closed if prior receipt/result evidence exists; recover that exact state before creating another payment context.

Raw request / response

live

Create a workspace, then stage the unpaid and paid requests here.

Live transaction evidence

Stellar testnet · opens in stellar.expert

Create a workspace to see the authorization and payment transactions.

Run both reference agents

npm run agents:testnet

One 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 challenge
Contractexecute_payment re-checks and consumes the mandate
ProofThe chain-derived mandate agent signs that request and transaction
HTTP 200Only a verified payment unlocks the resource

Keep 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.