Skip to content

Commit ca3ea92

Browse files
authored
Merge pull request #548 from Merit-Systems/aec/refund
feat: refund failed x402 Sora 2 video gen
2 parents 9584440 + 05e59cf commit ca3ea92

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- CreateTable
2+
CREATE TABLE "video_generation_x402" (
3+
"videoId" TEXT NOT NULL,
4+
"wallet" TEXT,
5+
"userId" UUID,
6+
"echoAppId" UUID,
7+
"cost" DECIMAL(65,30) NOT NULL,
8+
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9+
"expiresAt" TIMESTAMPTZ(6) NOT NULL,
10+
"isFinal" BOOLEAN NOT NULL DEFAULT false,
11+
12+
CONSTRAINT "video_generation_x402_pkey" PRIMARY KEY ("videoId")
13+
);
14+
15+
-- AddForeignKey
16+
ALTER TABLE "video_generation_x402" ADD CONSTRAINT "video_generation_x402_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
17+
18+
-- AddForeignKey
19+
ALTER TABLE "video_generation_x402" ADD CONSTRAINT "video_generation_x402_echoAppId_fkey" FOREIGN KEY ("echoAppId") REFERENCES "echo_apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;

packages/app/control/prisma/schema.prisma

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ model User {
4040
latestFreeCreditsVersion Decimal?
4141
OutboundEmailSent OutboundEmailSent[]
4242
creditGrantCodeUsages CreditGrantCodeUsage[]
43+
VideoGenerationX402 VideoGenerationX402[]
4344
4445
@@map("users")
4546
}
@@ -107,6 +108,7 @@ model EchoApp {
107108
appSessions AppSession[]
108109
payouts Payout[]
109110
OutboundEmailSent OutboundEmailSent[]
111+
VideoGenerationX402 VideoGenerationX402[]
110112
111113
@@map("echo_apps")
112114
}
@@ -489,3 +491,18 @@ model OutboundEmailSent {
489491
@@index([emailCampaignId])
490492
@@map("outbound_emails_sent")
491493
}
494+
495+
model VideoGenerationX402 {
496+
videoId String @id
497+
wallet String?
498+
userId String? @db.Uuid
499+
echoAppId String? @db.Uuid
500+
cost Decimal
501+
createdAt DateTime @default(now()) @db.Timestamptz(6)
502+
expiresAt DateTime @db.Timestamptz(6)
503+
isFinal Boolean @default(false)
504+
505+
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
506+
echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade)
507+
@@map("video_generation_x402")
508+
}

packages/app/server/src/handlers.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { Decimal } from '@prisma/client/runtime/library';
2525
import logger from 'logger';
2626
import { Request, Response } from 'express';
27+
import { ProviderType } from 'providers/ProviderType';
2728

2829
export async function refund(
2930
paymentAmountDecimal: Decimal,
@@ -154,6 +155,19 @@ export async function handleX402Request({
154155
provider,
155156
isStream
156157
);
158+
const transaction = transactionResult.transaction;
159+
160+
161+
if (provider.getType() === ProviderType.OPENAI_VIDEOS) {
162+
await prisma.videoGenerationX402.create({
163+
data: {
164+
videoId: transaction.metadata.providerId,
165+
wallet: payload.authorization.from,
166+
cost: transaction.rawTransactionCost,
167+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 1),
168+
},
169+
});
170+
}
157171

158172
modelRequestService.handleResolveResponse(
159173
res,
@@ -212,10 +226,25 @@ export async function handleApiKeyRequest({
212226
isStream
213227
);
214228

229+
230+
215231
// There is no actual refund, this logs if we underestimate the raw cost
216232
calculateRefundAmount(maxCost, transaction.rawTransactionCost);
217233

218234
modelRequestService.handleResolveResponse(res, isStream, data);
219235

220236
await echoControlService.createTransaction(transaction, maxCost);
237+
238+
if (provider.getType() === ProviderType.OPENAI_VIDEOS) {
239+
const transactionCost = await echoControlService.computeTransactionCosts(transaction, null);
240+
await prisma.videoGenerationX402.create({
241+
data: {
242+
videoId: transaction.metadata.providerId,
243+
userId: echoControlService.getUserId()!,
244+
echoAppId: echoControlService.getEchoAppId()!,
245+
cost: transactionCost.totalTransactionCost,
246+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 1),
247+
},
248+
});
249+
}
221250
}

packages/app/server/src/providers/OpenAIVideoProvider.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { Request } from 'express';
44
import { ProviderType } from './ProviderType';
55
import { EscrowRequest } from '../middleware/transaction-escrow-middleware';
66
import { Response } from 'express';
7+
import { transfer } from 'transferWithAuth';
78
import { getVideoModelPrice } from 'services/AccountingService';
89
import { HttpError, UnknownModelError } from 'errors/http';
910
import { Decimal } from 'generated/prisma/runtime/library';
1011
import { Transaction } from '../types';
1112
import { prisma } from '../server';
1213
import { EchoDbService } from '../services/DbService';
1314
import logger from '../logger';
15+
import { decimalToUsdcBigInt } from 'utils';
1416

1517
export class OpenAIVideoProvider extends BaseProvider {
1618
static detectPassthroughProxy(
@@ -173,9 +175,72 @@ export class OpenAIVideoProvider extends BaseProvider {
173175
}
174176

175177
const responseData = await response.json();
178+
switch (responseData.status) {
179+
case 'completed':
180+
await this.handleSuccessfulVideoGeneration(responseData.id as string);
181+
break;
182+
case 'failed':
183+
await this.handleFailedVideoGeneration(responseData.id as string);
184+
break;
185+
default:
186+
break;
187+
}
176188
res.json(responseData);
177189
}
178190

191+
// ====== Refund methods ======
192+
private async handleSuccessfulVideoGeneration(
193+
videoId: string
194+
): Promise<void> {
195+
await prisma.$transaction(async tx => {
196+
const result = await tx.$queryRawUnsafe(
197+
`SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`,
198+
videoId
199+
);
200+
const video = (result as any[])[0];
201+
if (video && !video.isFinal) {
202+
await tx.videoGenerationX402.update({
203+
where: {
204+
videoId: video.videoId,
205+
},
206+
data: {
207+
isFinal: true,
208+
},
209+
});
210+
}
211+
});
212+
}
213+
214+
private async handleFailedVideoGeneration(videoId: string): Promise<void> {
215+
await prisma.$transaction(async tx => {
216+
const result = await tx.$queryRawUnsafe(
217+
`SELECT * FROM "video_generation_x402" WHERE "videoId" = $1 FOR UPDATE`,
218+
videoId
219+
);
220+
const video = (result as any[])[0];
221+
// Exit early if video already final
222+
if (!video || video.isFinal) {
223+
return;
224+
}
225+
if (video.wallet) {
226+
const refundAmount = decimalToUsdcBigInt(video.cost);
227+
await transfer(video.wallet as `0x${string}`, refundAmount);
228+
}
229+
if (video.userId) {
230+
// Proccess the refund to the user. There is some level of complexity here since there is a markup. Not as simple as just credit grant.
231+
logger.info(`Refunding video generation ${video.videoId} to user ${video.userId} on app ${video.echoAppId}`);
232+
}
233+
await tx.videoGenerationX402.update({
234+
where: {
235+
videoId: video.videoId,
236+
},
237+
data: {
238+
isFinal: true,
239+
},
240+
});
241+
});
242+
}
243+
179244
// ========== Video Download Handling ==========
180245

181246
private isVideoContentDownload(path: string): boolean {

0 commit comments

Comments
 (0)