diff --git a/bun.lock b/bun.lock
index 8f93a8a..377380c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -14,6 +14,7 @@
"@profullstack/autoblog": "^0.5.0",
"@profullstack/coinpay": "^0.25.0",
"@profullstack/coinpay-onboard": "^0.5.0",
+ "@profullstack/cross-promo": "^0.1.0",
"@profullstack/fleetbar": "0.2.0",
"@profullstack/footer": "latest",
"@profullstack/hqtui": "^0.11.0",
@@ -325,6 +326,8 @@
"@profullstack/coinpay-onboard": ["@profullstack/coinpay-onboard@0.5.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.32.2", "zod": "^4.6.7" }, "bin": { "coinpay-onboard": "bin/coinpay-onboard" } }, "sha512-yqSx3pgJLP521Km+XNNn9fyjbbHppgozuGTtzTln9yy+2I0h0bjqXSpsFPwoSgVb5QcyBx+Wzv5170Qb7va1iQ=="],
+ "@profullstack/cross-promo": ["@profullstack/cross-promo@0.1.0", "", { "bin": { "cross-promo": "bin/cross-promo.js" } }, "sha512-LQMxZSvApH1T+8WmMu/Nj1LqojXeTw6wAtpLfhuKFwx9eR36hQG/7vVIDkKpmU5zKw7UwARe20MOfFCak2+ejA=="],
+
"@profullstack/emailer": ["@profullstack/emailer@1.0.3", "", {}, "sha512-VyAtCcfnSI6LOwJvWogCr3ZNmEsaZw+iuWrqnosLsdcKsbRyCrCmXigMNZJZEjlgYlR+lK//PwNPG+xtCFvWAw=="],
"@profullstack/fleetbar": ["@profullstack/fleetbar@0.2.0", "", {}, "sha512-ImALhrAVZUnc0aqFW518WVZX895ffnLmChC/KXR6mPqONFinoLI2wwWASSN9n5wJrjsJShU6nTebrUXEX4GdCQ=="],
diff --git a/lib/email.ts b/lib/email.ts
index f6c9fc3..7e422bd 100644
--- a/lib/email.ts
+++ b/lib/email.ts
@@ -1,4 +1,5 @@
import { createEmailer, type Emailer } from "@profullstack/stack/email";
+import { promoBlock } from "@profullstack/cross-promo";
import { env } from "./env";
import type { SenderMailbox } from "./outreach/senderMailbox";
@@ -252,7 +253,14 @@ export function purchaseReceiptEmailHtml(input: {
currency: string;
paymentId: string;
txHash?: string | null;
+ /** Cross-promo block (receiptPromo().html), shown after the receipt, above the footer. */
+ promoHtml?: string;
}): string {
+ const promoRow = input.promoHtml
+ ? `
+ | ${input.promoHtml} |
+
`
+ : "";
const txRow = input.txHash
? `
| Tx |
@@ -307,7 +315,8 @@ export function purchaseReceiptEmailHtml(input: {
A PDF copy of this receipt is attached for your records.
-
`;
+
+ ${promoRow}`;
return emailShell({
title: `Your CrawlProof receipt`,
innerHtml,
@@ -445,6 +454,24 @@ export async function sendMarketingEmail(input: {
return { sent: true };
}
+// The "more from the network" block for a credits buyer: a publisher. crawlproof.com is the
+// sender, so it never promotes itself. Themed to the dark email shell. Never throws.
+export function receiptPromo(to: string) {
+ return promoBlock({
+ site: "crawlproof.com",
+ audience: "publishers",
+ seed: to,
+ theme: {
+ accent: "#6ee7b7",
+ text: "#9aa3b2",
+ muted: "#64748b",
+ bg: "#0b0d10",
+ border: "#1f2630",
+ heading: "#e7e9ee",
+ },
+ });
+}
+
export async function sendPurchaseReceiptEmail(input: {
to: string;
paymentId: string;
@@ -463,6 +490,7 @@ export async function sendPurchaseReceiptEmail(input: {
minimumFractionDigits: input.amountCents % 100 === 0 ? 0 : 2,
})}`;
const filename = `crawlproof-receipt-${input.paymentId.slice(0, 12)}.pdf`;
+ const promo = await receiptPromo(input.to);
const res = await c.send({
from: env.resendFrom,
@@ -475,6 +503,7 @@ export async function sendPurchaseReceiptEmail(input: {
currency: input.currency,
paymentId: input.paymentId,
txHash: input.txHash,
+ promoHtml: promo.html,
}),
attachments: [{ filename, content: input.pdf.toString("base64") }],
});
diff --git a/package.json b/package.json
index f4761e0..f4e3cd3 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@profullstack/autoblog": "^0.5.0",
"@profullstack/coinpay": "^0.25.0",
"@profullstack/coinpay-onboard": "^0.5.0",
+ "@profullstack/cross-promo": "^0.1.0",
"@profullstack/fleetbar": "0.2.0",
"@profullstack/footer": "latest",
"@profullstack/hqtui": "^0.11.0",
diff --git a/tests/email-cross-promo.test.ts b/tests/email-cross-promo.test.ts
new file mode 100644
index 0000000..faa3fcd
--- /dev/null
+++ b/tests/email-cross-promo.test.ts
@@ -0,0 +1,61 @@
+import { beforeAll, describe, expect, it, vi } from "vitest";
+
+// The cross-promo block reads its bundled catalog, never the CDN, in tests.
+process.env.CROSS_PROMO = "bundled";
+process.env.RESEND_API_KEY ||= "re_test";
+
+const sent: Array<{ to: string; subject: string; html: string }> = [];
+vi.mock("@profullstack/stack/email", () => ({
+ createEmailer: () => ({
+ send: vi.fn(async (m: { to: string; subject: string; html: string }) => {
+ sent.push(m);
+ return { sent: true };
+ }),
+ }),
+}));
+
+let email: typeof import("@/lib/email");
+beforeAll(async () => {
+ email = await import("@/lib/email");
+});
+
+describe("purchase receipt cross-promo", () => {
+ it("promotes publisher sites to a credits buyer, never crawlproof.com itself", async () => {
+ const promo = await email.receiptPromo("buyer@example.com");
+ expect(promo.items.length).toBeGreaterThan(0);
+ expect(promo.items.map((i) => i.id)).toContain("rssamplifier.com");
+ expect(promo.items.map((i) => i.id)).not.toContain("crawlproof.com");
+ expect(promo.html).not.toContain('href="https://crawlproof.com');
+ expect(promo.html).toContain("utm_source=crawlproof.com");
+ });
+
+ it("puts the block in the receipt after the billing link", async () => {
+ const res = await email.sendPurchaseReceiptEmail({
+ to: "buyer@example.com",
+ paymentId: "pay_1234567890abcdef",
+ packLabel: "Starter",
+ creditsAdded: 10,
+ amountCents: 1000,
+ currency: "USD",
+ completedAt: new Date().toISOString(),
+ pdf: Buffer.from("%PDF-1.4"),
+ });
+ expect(res.sent).toBe(true);
+ const html = sent.at(-1)!.html;
+ expect(html).toContain("Grow your readership");
+ expect(html).toContain("https://rssamp.com");
+ expect(html).not.toContain('href="https://crawlproof.com/?utm');
+ expect(html.indexOf("View billing")).toBeLessThan(html.indexOf("Grow your readership"));
+ });
+
+ it("renders the receipt unchanged without a promo", () => {
+ const html = email.purchaseReceiptEmailHtml({
+ packLabel: "Starter",
+ creditsAdded: 1,
+ amountFormatted: "$1",
+ currency: "USD",
+ paymentId: "pay_x",
+ });
+ expect(html).not.toContain("Grow your readership");
+ });
+});