tantk commited on
Commit
f8b3b76
·
1 Parent(s): 5dbe2f3

fix: extract payer from tx XDR, refund on crash, memo length guard, MCP refund info

Browse files
Files changed (3) hide show
  1. mcp-server.js +1 -1
  2. refund.js +1 -1
  3. server.js +24 -6
mcp-server.js CHANGED
@@ -102,7 +102,7 @@ server.tool(
102
  content: [
103
  {
104
  type: "text",
105
- text: `# ${result.title}\n\nURL: ${result.url}\nRendered: ${result.renderedAt}\nRender time: ${result.renderTimeMs}ms\nPayment: ${result.payment?.price} on ${result.payment?.network}\n\n${result.content}`,
106
  },
107
  ],
108
  };
 
102
  content: [
103
  {
104
  type: "text",
105
+ text: `# ${result.title}\n\nURL: ${result.url}\nRendered: ${result.renderedAt}\nRender time: ${result.renderTimeMs}ms\nPayment: ${result.payment?.price} on ${result.payment?.network}${result.refund ? `\nRefund: ${result.refund.amount} — ${result.refund.reason} (tx: ${result.refund.transaction})` : ""}\n\n${result.content}`,
106
  },
107
  ],
108
  };
refund.js CHANGED
@@ -76,7 +76,7 @@ export async function sendRefund(payerAddress, amount, memo) {
76
  amount: amount,
77
  }),
78
  )
79
- .addMemo(TransactionBuilder.memo("text", memo || "RenderGate refund"))
80
  .setTimeout(30)
81
  .build();
82
 
 
76
  amount: amount,
77
  }),
78
  )
79
+ .addMemo(TransactionBuilder.memo("text", (memo || "RenderGate refund").slice(0, 28)))
80
  .setTimeout(30)
81
  .build();
82
 
server.js CHANGED
@@ -3,6 +3,7 @@ import express from "express";
3
  import { paymentMiddlewareFromConfig } from "@x402/express";
4
  import { HTTPFacilitatorClient } from "@x402/core/server";
5
  import { ExactStellarScheme } from "@x402/stellar/exact/server";
 
6
  import { renderUrl, closeBrowser } from "./renderer.js";
7
  import { isFailedRender, sendRefund } from "./refund.js";
8
 
@@ -90,13 +91,18 @@ app.use(
90
  ),
91
  );
92
 
93
- // Extract payer address from the PAYMENT-RESPONSE header set by x402 middleware
94
- function getPayerFromResponse(res) {
95
  try {
96
- const header = res.getHeader("PAYMENT-RESPONSE");
 
97
  if (!header) return null;
98
- const decoded = JSON.parse(Buffer.from(header, "base64").toString());
99
- return decoded?.payer || null;
 
 
 
 
100
  } catch {
101
  return null;
102
  }
@@ -114,7 +120,7 @@ app.get("/render", async (req, res) => {
114
 
115
  // Check if the render actually succeeded
116
  const failReason = isFailedRender(result.content, result.title);
117
- const payerAddress = getPayerFromResponse(res);
118
  if (failReason && payerAddress) {
119
  console.log(`Bad render (${failReason}) for ${decoded} — refunding ${payerAddress}`);
120
  const refundHash = await sendRefund(payerAddress, "0.001", `refund:${failReason}`);
@@ -142,6 +148,18 @@ app.get("/render", async (req, res) => {
142
  if (err.message.includes("Too many concurrent")) {
143
  return res.status(503).json({ error: err.message });
144
  }
 
 
 
 
 
 
 
 
 
 
 
 
145
  res.status(500).json({ error: "Render failed", message: err.message });
146
  }
147
  });
 
3
  import { paymentMiddlewareFromConfig } from "@x402/express";
4
  import { HTTPFacilitatorClient } from "@x402/core/server";
5
  import { ExactStellarScheme } from "@x402/stellar/exact/server";
6
+ import { Transaction, Networks } from "@stellar/stellar-sdk";
7
  import { renderUrl, closeBrowser } from "./renderer.js";
8
  import { isFailedRender, sendRefund } from "./refund.js";
9
 
 
91
  ),
92
  );
93
 
94
+ // Extract payer address from the incoming payment-signature request header
95
+ function getPayerAddress(req) {
96
  try {
97
+ const header =
98
+ req.get("payment-signature") || req.get("x-payment") || req.get("PAYMENT-SIGNATURE");
99
  if (!header) return null;
100
+ const decoded = JSON.parse(Buffer.from(header, "base64url").toString());
101
+ // Decode the transaction XDR to get the source account (payer)
102
+ const txXdr = decoded?.payload?.transaction;
103
+ if (!txXdr) return null;
104
+ const tx = new Transaction(txXdr, Networks.TESTNET);
105
+ return tx.source;
106
  } catch {
107
  return null;
108
  }
 
120
 
121
  // Check if the render actually succeeded
122
  const failReason = isFailedRender(result.content, result.title);
123
+ const payerAddress = getPayerAddress(req);
124
  if (failReason && payerAddress) {
125
  console.log(`Bad render (${failReason}) for ${decoded} — refunding ${payerAddress}`);
126
  const refundHash = await sendRefund(payerAddress, "0.001", `refund:${failReason}`);
 
148
  if (err.message.includes("Too many concurrent")) {
149
  return res.status(503).json({ error: err.message });
150
  }
151
+ // Attempt refund on crash — agent paid but got nothing
152
+ const payerAddress = getPayerAddress(req);
153
+ if (payerAddress) {
154
+ const refundHash = await sendRefund(payerAddress, "0.001", "refund:render_crash");
155
+ return res.status(500).json({
156
+ error: "Render failed",
157
+ message: err.message,
158
+ refund: refundHash
159
+ ? { transaction: refundHash, amount: "0.001 USDC", reason: "render_crash" }
160
+ : { error: "Refund failed — contact support" },
161
+ });
162
+ }
163
  res.status(500).json({ error: "Render failed", message: err.message });
164
  }
165
  });