Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateEnum
CREATE TYPE "EnumTransactionType" AS ENUM ('X402', 'BALANCE');

-- DropForeignKey
ALTER TABLE "public"."transactions" DROP CONSTRAINT "transactions_echoAppId_fkey";

-- AlterTable
ALTER TABLE "transactions" ADD COLUMN "transactionType" "EnumTransactionType" NOT NULL DEFAULT 'BALANCE',
ALTER COLUMN "userId" DROP NOT NULL,
ALTER COLUMN "echoAppId" DROP NOT NULL;

-- AddForeignKey
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_echoAppId_fkey" FOREIGN KEY ("echoAppId") REFERENCES "echo_apps"("id") ON DELETE SET NULL ON UPDATE CASCADE;
16 changes: 11 additions & 5 deletions packages/app/control/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ enum EnumPaymentSource {
balance
}

enum EnumTransactionType {
X402
BALANCE
}

model Transaction {
id String @id @default(uuid()) @db.Uuid
transactionMetadataId String? @db.Uuid
Expand All @@ -313,20 +318,21 @@ model Transaction {
isArchived Boolean @default(false)
archivedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
userId String @db.Uuid
echoAppId String @db.Uuid
transactionType EnumTransactionType @default(BALANCE)
userId String? @db.Uuid
echoAppId String? @db.Uuid
apiKeyId String? @db.Uuid
markUpId String? @db.Uuid
spendPoolId String? @db.Uuid
userSpendPoolUsageId String? @db.Uuid
referralCodeId String? @db.Uuid
referrerRewardId String? @db.Uuid
apiKey ApiKey? @relation(fields: [apiKeyId], references: [id], onDelete: Cascade)
echoApp EchoApp @relation(fields: [echoAppId], references: [id])
echoApp EchoApp? @relation(fields: [echoAppId], references: [id])
markUp MarkUp? @relation(fields: [markUpId], references: [id])
spendPool SpendPool? @relation(fields: [spendPoolId], references: [id])
transactionMetadata TransactionMetadata? @relation(fields: [transactionMetadataId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userSpendPoolUsage UserSpendPoolUsage? @relation(fields: [userSpendPoolUsageId], references: [id])
referralCode ReferralCode? @relation(fields: [referralCodeId], references: [id])
referrerReward ReferralReward? @relation(fields: [referrerRewardId], references: [id])
Expand Down Expand Up @@ -506,4 +512,4 @@ model VideoGenerationX402 {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade)
@@map("video_generation_x402")
}
}
6 changes: 3 additions & 3 deletions packages/app/control/src/services/db/apps/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ export const listAppTransactions = async (
groupedTransactions.set(userKey, {
id: transaction.id,
user: {
id: transaction.userId,
name: transaction.user.name,
image: transaction.user.image,
id: transaction.userId!,
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
name: transaction.user?.name ?? null,
image: transaction.user?.image ?? null,
},
callCount: 1,
markUpProfit: Number(transaction.markUpProfit),
Expand Down
3 changes: 3 additions & 0 deletions packages/app/control/src/services/db/apps/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const listAppUsers = async (
{ page, page_size }: PaginationParams
) => {
const where: Prisma.TransactionWhereInput = {
userId: {
not: null,
},
echoAppId: appId,
isArchived: false,
...((startDate !== undefined || endDate !== undefined) && {
Expand Down
12 changes: 7 additions & 5 deletions packages/app/server/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TransactionEscrowMiddleware } from 'middleware/transaction-escrow-middleware';
import { modelRequestService } from 'services/ModelRequestService';
import { HandlerInput, Network, Transaction, X402HandlerInput } from 'types';
import { ApiKeyHandlerInput, Network, Transaction, X402HandlerInput } from 'types';
import {
usdcBigIntToDecimal,
decimalToUsdcBigInt,
Expand Down Expand Up @@ -168,11 +168,11 @@ export async function handleX402Request({
isPassthroughProxyRoute,
provider,
isStream,
x402AuthenticationService,
}: X402HandlerInput) {
if (isPassthroughProxyRoute) {
return await makeProxyPassthroughRequest(req, res, provider, headers);
}

const settleResult = await settle(req, res, headers, maxCost);
if (!settleResult) {
return;
Expand All @@ -189,7 +189,6 @@ export async function handleX402Request({
isStream
);
const transaction = transactionResult.transaction;

if (provider.getType() === ProviderType.OPENAI_VIDEOS) {
await prisma.videoGenerationX402.create({
data: {
Expand All @@ -207,6 +206,9 @@ export async function handleX402Request({
transactionResult.data
);

logger.info(`Creating X402 transaction for app. Metadata: ${JSON.stringify(transaction.metadata)}`);
await x402AuthenticationService.createX402Transaction(transaction);

await finalize(
paymentAmountDecimal,
transactionResult.transaction,
Expand All @@ -226,7 +228,7 @@ export async function handleApiKeyRequest({
isPassthroughProxyRoute,
provider,
isStream,
}: HandlerInput) {
}: ApiKeyHandlerInput) {
const transactionEscrowMiddleware = new TransactionEscrowMiddleware(prisma);

if (isPassthroughProxyRoute) {
Expand Down Expand Up @@ -263,7 +265,7 @@ export async function handleApiKeyRequest({

modelRequestService.handleResolveResponse(res, isStream, data);

await echoControlService.createTransaction(transaction, maxCost);
await echoControlService.createTransaction(transaction);

if (provider.getType() === ProviderType.OPENAI_VIDEOS) {
const transactionCost = await echoControlService.computeTransactionCosts(
Expand Down
2 changes: 1 addition & 1 deletion packages/app/server/src/resources/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function handleApiRequest<TInput, TOutput>(
const actualCost = calculateActualCost(parsedBody, output);
const transaction = createTransaction(parsedBody, output, actualCost);

await echoControlService.createTransaction(transaction, actualCost);
await echoControlService.createTransaction(transaction);

return output;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/app/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from './services/PricingService';
import { Decimal } from '@prisma/client/runtime/library';
import resourceRouter from './routers/resource';
import { X402AuthenticationService } from 'services/x402AuthenticationService';

dotenv.config();

Expand Down Expand Up @@ -107,11 +108,14 @@ app.all('*', async (req: EscrowRequest, res: Response, next: NextFunction) => {
const headers = req.headers as Record<string, string>;
const { provider, isStream, isPassthroughProxyRoute, is402Sniffer } =
await initializeProvider(req, res);

const x402AuthenticationService = new X402AuthenticationService(prisma);
const x402AuthenticationResult = await x402AuthenticationService.authenticateX402Request(headers);
if (!provider || is402Sniffer) {
return buildX402Response(req, res, new Decimal(0));
}
const maxCost = getRequestMaxCost(req, provider, isPassthroughProxyRoute);
const maxCostWithMarkup = applyMaxCostMarkup(maxCost);
const maxCostWithMarkup = applyMaxCostMarkup(maxCost, x402AuthenticationResult?.markUp || null);

if (
!isApiRequest(headers) &&
Expand Down Expand Up @@ -148,6 +152,7 @@ app.all('*', async (req: EscrowRequest, res: Response, next: NextFunction) => {
isPassthroughProxyRoute,
provider,
isStream,
x402AuthenticationService
});
return;
}
Expand Down
51 changes: 39 additions & 12 deletions packages/app/server/src/services/DbService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TransactionRequest,
isLlmTransactionMetadata,
isVeoTransactionMetadata,
EchoApp,
} from '../types';
import { createHmac } from 'crypto';
import { jwtVerify } from 'jose';
Expand Down Expand Up @@ -412,13 +413,14 @@ export class EchoDbService {
transaction
);

// Update user's total spent amount
Comment thread
vercel[bot] marked this conversation as resolved.
await this.updateUserTotalSpent(
tx,
transaction.userId,
transaction.totalCost
);

if (transaction.userId) {
// Update user's total spent amount
await this.updateUserTotalSpent(
tx,
transaction.userId,
transaction.totalCost
);
}
// Update API key's last used timestamp if provided
if (transaction.apiKeyId) {
await this.updateApiKeyLastUsed(tx, transaction.apiKeyId);
Expand Down Expand Up @@ -449,7 +451,7 @@ export class EchoDbService {
spendPoolId: string
): Promise<{
transaction: Transaction;
userSpendPoolUsage: UserSpendPoolUsage;
userSpendPoolUsage: UserSpendPoolUsage | null;
}> {
try {
return await this.db.$transaction(async tx => {
Expand All @@ -462,15 +464,14 @@ export class EchoDbService {
if (!spendPool) {
throw new Error('Spend pool not found');
}

// 2. Upsert UserSpendPoolUsage record using helper
const userSpendPoolUsage = await this.upsertUserSpendPoolUsage(
const userSpendPoolUsage = transactionData.userId ?
await this.upsertUserSpendPoolUsage(
tx,
transactionData.userId,
spendPoolId,
transactionData.totalCost
);

) : null;
// 3. Create the transaction record
const transaction = await this.createTransactionRecord(
tx,
Expand Down Expand Up @@ -522,4 +523,30 @@ export class EchoDbService {

return !!transaction;
}

async getEchoAppById(echoAppId: string): Promise<EchoApp | null> {
const echoApp = await this.db.echoApp.findUnique({
where: { id: echoAppId },
});
if (!echoApp) {
return null;
}
return {
id: echoApp.id,
name: echoApp.name,
createdAt: echoApp.createdAt.toISOString(),
updatedAt: echoApp.updatedAt.toISOString(),
};
}
async getCurrentMarkupByEchoAppId(echoAppId: string) {
const echoApp = await this.getEchoAppById(echoAppId);
if (!echoApp) {
return null;
}
const markup = await this.db.echoApp.findUnique({
where: { id: echoAppId },
select: { markUp: true },
});
return markup?.markUp || null;
}
}
Loading
Loading