diff --git a/apps/pay/src/monitor.ts b/apps/pay/src/monitor.ts
index 171efec..d70a236 100644
--- a/apps/pay/src/monitor.ts
+++ b/apps/pay/src/monitor.ts
@@ -3,6 +3,7 @@
import sql from "./db";
import { getAddressInfo, getAddressInfoBulk } from "./freedom";
import { COINS } from "./plans";
+import { generateReceipt } from "./receipt";
const SOCK_API = process.env.FREEDOM_SOCK ?? "https://sock-v1.freedom.st";
const THRESHOLD = 0.95;
@@ -83,6 +84,7 @@ async function evaluatePayment(paymentId: number) {
if (newStatus === "paid") {
await sql`UPDATE payments SET status = 'paid', amount_received = ${received.toFixed(8)}, paid_at = now() WHERE id = ${paymentId} AND status != 'paid'`;
await applyPlan(payment);
+ await generateReceipt(paymentId).catch(e => console.error(`Receipt generation failed for ${paymentId}:`, e));
addressMap.delete(payment.address);
console.log(`Payment ${paymentId} paid`);
} else {
diff --git a/apps/pay/src/receipt.ts b/apps/pay/src/receipt.ts
new file mode 100644
index 0000000..0d7ac3f
--- /dev/null
+++ b/apps/pay/src/receipt.ts
@@ -0,0 +1,129 @@
+import sql from "./db";
+import { COINS } from "./plans";
+
+export async function generateReceipt(paymentId: number): Promise {
+ const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
+ if (!payment) throw new Error("Payment not found");
+
+ // Already locked — return as-is
+ if (payment.receipt_html) return payment.receipt_html;
+
+ const coinInfo = COINS[payment.coin];
+ const txs = await sql`
+ SELECT txid, amount, confirmed, detected_at
+ FROM payment_txs WHERE payment_id = ${paymentId}
+ ORDER BY detected_at ASC
+ `;
+
+ const paidDate = payment.paid_at
+ ? new Date(payment.paid_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
+ : "—";
+ const createdDate = new Date(payment.created_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
+ const planLabel = payment.plan === "lifetime"
+ ? "Lifetime"
+ : `Pro × ${payment.months} month${payment.months > 1 ? "s" : ""}`;
+
+ const txRows = txs.map((tx: any) => {
+ const date = new Date(tx.detected_at).toLocaleDateString("en-US", {
+ year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
+ });
+ return `
+ ${tx.txid}
+ ${tx.amount} ${coinInfo?.ticker || payment.coin.toUpperCase()}
+ ${date}
+ `;
+ }).join("");
+
+ const html = `
+
+
+
+
+ PingQL Receipt #${payment.id}
+
+
+
+
+ Print Receipt
+
+
+
+
+
+
Plan
+
+ Product
+ PingQL ${planLabel}
+
+
+ Invoice Date
+ ${createdDate}
+
+
+ Payment Date
+ ${paidDate}
+
+
+
+
+
Payment
+
+ Currency
+ ${coinInfo?.label || payment.coin} (${coinInfo?.ticker || payment.coin.toUpperCase()})
+
+
+ Amount Paid
+ ${payment.amount_received || payment.amount_crypto} ${coinInfo?.ticker || payment.coin.toUpperCase()}
+
+
+ Payment Address
+ ${payment.address}
+
+
+ Total (USD)
+ $${Number(payment.amount_usd).toFixed(2)}
+
+
+
+ ${txs.length > 0 ? `
+
Transactions
+
+ Transaction ID Amount Date
+ ${txRows}
+
+
` : ""}
+
+
+
+`;
+
+ // Lock it
+ await sql`UPDATE payments SET receipt_html = ${html} WHERE id = ${paymentId}`;
+ return html;
+}
diff --git a/apps/pay/src/routes.ts b/apps/pay/src/routes.ts
index aec489a..f81eb6a 100644
--- a/apps/pay/src/routes.ts
+++ b/apps/pay/src/routes.ts
@@ -3,6 +3,7 @@ import sql from "./db";
import { derive } from "./address";
import { getExchangeRates, getAvailableCoins, fetchQrBase64 } from "./freedom";
import { PLANS, COINS } from "./plans";
+import { generateReceipt } from "./receipt";
import { watchPayment } from "./monitor";
// Resolve account from key (same logic as API/web apps)
@@ -178,7 +179,7 @@ export const routes = new Elysia()
};
})
- // Generate or serve a locked receipt for a paid invoice
+ // Serve locked receipt for a paid invoice
.get("/checkout/:id/receipt", async ({ accountId, params, set }) => {
const [payment] = await sql`
SELECT * FROM payments WHERE id = ${params.id} AND account_id = ${accountId}
@@ -186,129 +187,8 @@ export const routes = new Elysia()
if (!payment) { set.status = 404; return { error: "Payment not found" }; }
if (payment.status !== "paid") { set.status = 400; return { error: "Receipt is only available for paid invoices" }; }
- // If receipt already generated, serve it as-is (locked)
- if (payment.receipt_html) {
- set.headers["content-type"] = "text/html; charset=utf-8";
- return payment.receipt_html;
- }
-
- // Generate receipt
- const coinInfo = COINS[payment.coin];
- const txs = await sql`
- SELECT txid, amount, confirmed, detected_at
- FROM payment_txs WHERE payment_id = ${payment.id}
- ORDER BY detected_at ASC
- `;
-
- const paidDate = payment.paid_at
- ? new Date(payment.paid_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
- : "—";
- const createdDate = new Date(payment.created_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
- const planLabel = payment.plan === "lifetime" ? "Lifetime" : `Pro × ${payment.months} month${payment.months > 1 ? "s" : ""}`;
-
- const txRows = txs.map((tx: any) => {
- const date = new Date(tx.detected_at).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
- return `
- ${tx.txid}
- ${tx.amount} ${coinInfo?.ticker || payment.coin.toUpperCase()}
- ${date}
- `;
- }).join("");
-
- const receiptHtml = `
-
-
-
-
- PingQL Receipt #${payment.id}
-
-
-
-
- Print Receipt
-
-
-
-
-
-
Plan
-
- Product
- PingQL ${planLabel}
-
-
- Invoice Date
- ${createdDate}
-
-
- Payment Date
- ${paidDate}
-
-
-
-
-
Payment
-
- Currency
- ${coinInfo?.label || payment.coin} (${coinInfo?.ticker || payment.coin.toUpperCase()})
-
-
- Amount Paid
- ${payment.amount_received || payment.amount_crypto} ${coinInfo?.ticker || payment.coin.toUpperCase()}
-
-
- Payment Address
- ${payment.address}
-
-
- Total (USD)
- $${Number(payment.amount_usd).toFixed(2)}
-
-
-
- ${txs.length > 0 ? `
-
Transactions
-
- Transaction ID Amount Date
- ${txRows}
-
-
` : ""}
-
-
-
-`;
-
- // Lock the receipt by storing it
- await sql`UPDATE payments SET receipt_html = ${receiptHtml} WHERE id = ${payment.id}`;
-
set.headers["content-type"] = "text/html; charset=utf-8";
- return receiptHtml;
+ return payment.receipt_html || await generateReceipt(payment.id);
})
;
diff --git a/apps/web/src/views/landing.ejs b/apps/web/src/views/landing.ejs
index 54cb4ca..58c6962 100644
--- a/apps/web/src/views/landing.ejs
+++ b/apps/web/src/views/landing.ejs
@@ -423,10 +423,6 @@
-
-
- Hosted in EU (Prague) — GDPR compliant
-
diff --git a/apps/web/src/views/privacy.ejs b/apps/web/src/views/privacy.ejs
index 0f0a89d..302a894 100644
--- a/apps/web/src/views/privacy.ejs
+++ b/apps/web/src/views/privacy.ejs
@@ -118,11 +118,6 @@
Ping history is retained for 90 days by default. Account data is kept until you delete your account. If you want everything deleted, contact us and we'll do it.
- Where data is stored
-
- Servers are in Prague, Czech Republic (EU). GDPR applies.
-
-
Security
All data in transit is encrypted via TLS. All data at rest is on encrypted disks. Your account key is the only credential — there's no password database to breach, no hashed passwords to crack, no OAuth tokens to steal.