Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
79312ce
Add Tremendous payout setup to referrals embed
devkiran May 27, 2026
7052404
Queue Tremendous payouts after charge succeeded
devkiran May 27, 2026
2ff0a2e
Merge branch 'main' into tremendous
devkiran May 28, 2026
1155dd1
Send partner payouts via Tremendous gift card orders
devkiran May 28, 2026
9041268
Add Tremendous webhook endpoint with signature verification
devkiran May 28, 2026
5f65d40
Send Tremendous reward email with redeem link
devkiran May 28, 2026
c9a7fa1
Fix Tremendous payout order handling and webhook imports
devkiran May 28, 2026
c854df5
Fix the webhooks
devkiran May 28, 2026
9bdda0e
Remove Tremendous webhook secret and schema
devkiran May 28, 2026
fb77316
Merge branch 'main' into tremendous
devkiran May 28, 2026
11c1f0e
Format
devkiran May 28, 2026
f5c640b
Update Tremendous payout email template and rename to partner-tremend…
devkiran May 28, 2026
3e2baf2
Redesign referrals embed payout settings with gift card and cash options
devkiran May 28, 2026
e223f56
Update settings.tsx
devkiran May 28, 2026
bcfde33
Hide referrals embed settings tab for non-Tremendous payout methods
devkiran May 28, 2026
babf9f6
Merge branch 'main' into tremendous
devkiran May 29, 2026
a12ac17
Gate Tremendous payouts to enabled programs and move OTP routes
devkiran May 29, 2026
d9ea0c7
Improve Tremendous payout error handling and logging
devkiran May 29, 2026
40d6b6a
Add force withdrawal support for Tremendous payouts
devkiran May 29, 2026
83220ca
Update settings.tsx
devkiran May 29, 2026
f8f30d3
Update send-tremendous-payouts.ts
devkiran May 29, 2026
981ae66
Merge branch 'main' into tremendous
devkiran May 29, 2026
394fdb2
Remove tremendousRewardId and make tremendousOrderId unique
devkiran May 29, 2026
bb5c9ba
Merge branch 'tremendous' of https://github.com/dubinc/dub into treme…
devkiran May 29, 2026
55c7b58
Show Tremendous gift card payout method when connected
devkiran May 29, 2026
ed022ec
Add Tremendous to payout method recomputation and config
devkiran May 29, 2026
ee9c52f
Address CR comments
devkiran May 29, 2026
c814b2d
Fix build
devkiran May 29, 2026
aa64a21
Merge branch 'main' into tremendous
devkiran Jun 3, 2026
c33a74d
Gate Tremendous payouts by country, prohibited TLDs, and disposable d…
devkiran Jun 3, 2026
3723661
Cap Tremendous gift card payouts at $2,000 per payout.
devkiran Jun 3, 2026
1d4788f
Merge branch 'main' into tremendous
devkiran Jun 4, 2026
cd13567
Sync Tremendous prohibited email domains and block them in OTP flows
devkiran Jun 4, 2026
5b93503
Update get-payouts-timeseries.ts
devkiran Jun 4, 2026
fcbacce
Merge branch 'main' into tremendous
steven-tey Jun 6, 2026
18e77b9
Merge branch 'main' into tremendous
devkiran Jun 9, 2026
f00c471
Create Tremendous campaign per program
devkiran Jun 9, 2026
6ef9306
address CR feedback
devkiran Jun 9, 2026
d4294be
Update qstash-workflow.ts
devkiran Jun 9, 2026
f30c9a0
Update route.ts
devkiran Jun 9, 2026
4e9e94c
Merge branch 'main' into tremendous
steven-tey Jun 12, 2026
d6ba4f5
improve disposable email cron
steven-tey Jun 12, 2026
1d72b03
small tweaks
steven-tey Jun 12, 2026
93ccbab
final changes
steven-tey Jun 12, 2026
ff42411
Merge branch 'main' into tremendous
steven-tey Jun 12, 2026
f7afb66
Update create-tremendous-campaign.ts
steven-tey Jun 12, 2026
f7768f3
Merge branch 'main' into tremendous
steven-tey Jun 12, 2026
95ff569
address coderabbit feedback
steven-tey Jun 12, 2026
13ad0c4
dynamic settingsButton
steven-tey Jun 12, 2026
6b39ed5
fix coderabbit feedback
steven-tey Jun 13, 2026
b276d86
fix tremendousConfiguration
steven-tey Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_WEBHOOK_ID=

# Tremendous
TREMENDOUS_API_KEY=

# Program lander generation
FIRECRAWL_API_KEY=

Expand Down
82 changes: 53 additions & 29 deletions apps/web/app/(ee)/api/cron/disposable-emails/route.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,68 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { withCron } from "@/lib/cron/with-cron";
import { redis } from "@/lib/upstash";
import { log } from "@dub/utils";
import { NextResponse } from "next/server";
import { logAndRespond } from "../utils";

export const dynamic = "force-dynamic";

// Cron to update the disposable email domains list in Redis
// POST /api/cron/disposable-emails
// Sync disposable and Tremendous prohibited email domain blocklists into Redis.
// Runs every Monday at noon UTC (0 12 * * 1)
export async function POST(req: Request) {
try {
const rawBody = await req.text();
export const POST = withCron(async () => {
const [disposableRes, tremendousRes] = await Promise.allSettled([
fetch(
"https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf",
),
fetch("https://api.tremendous.com/prohibited_email_domains.txt"),
]);

await verifyQstashSignature({
req,
rawBody,
});
if (disposableRes.status === "rejected") {
console.warn(
`Failed to fetch disposable email domains list: ${disposableRes.status} ${disposableRes.reason}`,
);
}

const disposableEmails = await fetch(
"https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf",
if (tremendousRes.status === "rejected") {
console.warn(
`Failed to fetch Tremendous prohibited email domains list: ${tremendousRes.status} ${tremendousRes.reason}`,
);
}

const domains = (await disposableEmails.text()).split("\n").filter(Boolean);
const disposableDomains =
disposableRes.status === "fulfilled"
? (await disposableRes.value.text()).split("\n").filter(Boolean)
: [];

if (domains.length < 100) {
throw new Error("Disposable email domains list is too short");
}
const tremendousDomains =
tremendousRes.status === "fulfilled"
? (await tremendousRes.value.text())
.split("\n")
.map((d) => d.trim().toLowerCase())
.filter(Boolean)
: [];

// Use a temporary set to avoid emptying the old set
if (disposableDomains.length > 0) {
// Use temporary sets to avoid emptying the old sets
await redis.del("disposableEmailDomainsTmp");
await redis.sadd("disposableEmailDomainsTmp", ...(domains as [string]));
await redis.sadd(
"disposableEmailDomainsTmp",
...(disposableDomains as [string]),
);
await redis.rename("disposableEmailDomainsTmp", "disposableEmailDomains");
}

return NextResponse.json({ status: "OK" });
} catch (error) {
await log({
message: `Error updating disposable email domains list: ${error.message}`,
type: "cron",
});

return handleAndReturnErrorResponse(error);
if (tremendousDomains.length > 0) {
await redis.del("tremendousProhibitedEmailDomainsTmp");
await redis.sadd(
"tremendousProhibitedEmailDomainsTmp",
...(tremendousDomains as [string]),
);
await redis.rename(
"tremendousProhibitedEmailDomainsTmp",
"tremendousProhibitedEmailDomains",
);
}
}

return logAndRespond(
`Synced ${disposableDomains.length} disposable and ${tremendousDomains.length} Tremendous prohibited email domains.`,
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { qstash } from "@/lib/cron";
import { TREMENDOUS_MAX_PAYOUT_AMOUNT_CENTS } from "@/lib/tremendous/constants";
import { createTremendousCampaign } from "@/lib/tremendous/create-tremendous-campaign";
import { prisma } from "@dub/prisma";
import {
Invoice,
PartnerPayoutMethod,
PayoutMode,
PayoutStatus,
} from "@dub/prisma/client";
import { APP_DOMAIN_WITH_NGROK, chunk } from "@dub/utils";

const queue = qstash.queue({
queueName: "send-tremendous-payout",
});

export async function queueTremendousPayouts(
invoice: Pick<Invoice, "id" | "paymentMethod" | "payoutMode" | "programId">,
) {
if (invoice.payoutMode === "external" || !invoice.programId) {
return;
}

const program = await prisma.program.findUniqueOrThrow({
where: {
id: invoice.programId,
},
select: {
id: true,
name: true,
logo: true,
tremendousCampaignId: true,
},
});

if (!program.tremendousCampaignId) {
await createTremendousCampaign(program);
}

const partnersInCurrentInvoice = await prisma.payout.groupBy({
by: ["partnerId"],
where: {
invoiceId: invoice.id,
status: PayoutStatus.processing,
mode: PayoutMode.internal,
method: PartnerPayoutMethod.tremendous,
amount: {
lte: TREMENDOUS_MAX_PAYOUT_AMOUNT_CENTS,
},
partner: {
tremendousEmail: {
not: null,
},
payoutsEnabledAt: {
not: null,
},
},
},
});

if (partnersInCurrentInvoice.length === 0) {
console.log("No partners for sending via Tremendous, skipping...");
return;
}

const chunkedPartners = chunk(partnersInCurrentInvoice, 100);

for (let i = 0; i < chunkedPartners.length; i++) {
const partnersInChunk = chunkedPartners[i];

await Promise.all(
partnersInChunk.map(({ partnerId }) =>
queue.enqueueJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/payouts/send-tremendous-payout`,
deduplicationId: `${invoice.id}-${partnerId}`,
method: "POST",
body: {
partnerId,
invoiceId: invoice.id,
},
}),
),
);

console.log(
`Enqueued Tremendous payout for ${partnersInChunk.length} partners in chunk ${i + 1} of ${chunkedPartners.length}`,
);
}
}
3 changes: 3 additions & 0 deletions apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as z from "zod/v4";
import { logAndRespond } from "../../utils";
import { queueExternalPayouts } from "./queue-external-payouts";
import { queueStripePayouts } from "./queue-stripe-payouts";
import { queueTremendousPayouts } from "./queue-tremendous-payouts";
import { sendPaypalPayouts } from "./send-paypal-payouts";
import { scheduleDelayedStablecoinPayouts } from "./utils";

Expand Down Expand Up @@ -122,6 +123,8 @@ export async function POST(req: Request) {
sendPaypalPayouts(invoice),
// Queue external payouts
queueExternalPayouts(invoice),
// Queue Tremendous payouts
queueTremendousPayouts(invoice),
]);

return logAndRespond(
Expand Down
25 changes: 25 additions & 0 deletions apps/web/app/(ee)/api/cron/payouts/send-tremendous-payout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { withCron } from "@/lib/cron/with-cron";
import { sendTremendousPayouts } from "@/lib/tremendous/send-tremendous-payouts";
import * as z from "zod/v4";
import { logAndRespond } from "../../utils";

export const dynamic = "force-dynamic";

const inputSchema = z.object({
partnerId: z.string(),
invoiceId: z.string().optional(),
});

// POST /api/cron/payouts/send-tremendous-payout
export const POST = withCron(async ({ rawBody }) => {
const { partnerId, invoiceId } = inputSchema.parse(JSON.parse(rawBody));

await sendTremendousPayouts({
partnerId,
invoiceId,
});

return logAndRespond(
`Processed send-tremendous-payout job for partner ${partnerId}.`,
);
});
Loading
Loading