Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "x402_transaction_metadata" (
"id" UUID NOT NULL,
"resourcePath" TEXT NOT NULL,
"resourceArgs" JSONB NOT NULL,
"resourceResponse" JSONB NOT NULL,
"resourceError" JSONB NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,

CONSTRAINT "x402_transaction_metadata_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_id_fkey" FOREIGN KEY ("id") REFERENCES "x402_transaction_metadata"("id") ON DELETE CASCADE ON UPDATE CASCADE;
152 changes: 84 additions & 68 deletions packages/app/control/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ model User {
latestFreeCreditsVersion Decimal?
OutboundEmailSent OutboundEmailSent[]
creditGrantCodeUsages CreditGrantCodeUsage[]
VideoGenerationX402 VideoGenerationX402[]
VideoGenerationX402 VideoGenerationX402[]

@@map("users")
}
Expand Down Expand Up @@ -103,9 +103,9 @@ model EchoApp {
markUp MarkUp? // The markup rate for this app (one-to-one)
githubLink GithubLink? // The GitHub link for this app (one-to-one)
spendPools SpendPool[] // Spend pools associated with this app
currentReferralRewardId String? @db.Uuid // Reference to current active referral reward
currentReferralReward ReferralReward? @relation("CurrentReferralReward", fields: [currentReferralRewardId], references: [id])
ReferralRewards ReferralReward[] @relation("AppReferralRewards") // All referral rewards for this app
currentReferralRewardId String? @db.Uuid // Reference to current active referral reward
currentReferralReward ReferralReward? @relation("CurrentReferralReward", fields: [currentReferralRewardId], references: [id])
ReferralRewards ReferralReward[] @relation("AppReferralRewards") // All referral rewards for this app
appSessions AppSession[]
payouts Payout[]
OutboundEmailSent OutboundEmailSent[]
Expand Down Expand Up @@ -234,23 +234,23 @@ enum EnumPayoutType {
}

model Payout {
id String @id @default(uuid()) @db.Uuid
amount Decimal @db.Decimal(65, 14)
id String @id @default(uuid()) @db.Uuid
amount Decimal @db.Decimal(65, 14)
status EnumPayoutStatus
payoutBatchId String? @db.Uuid
payoutBatchId String? @db.Uuid
type EnumPayoutType
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
description String?
transactionId String?
senderAddress String?
recipientGithubLinkId String? @db.Uuid
recipientGithubLinkId String? @db.Uuid
recipientAddress String?
userId String? @db.Uuid // User ID if type is "referral"
echoAppId String? @db.Uuid // App ID if type is "markup"
recipientGithubLink GithubLink? @relation(fields: [recipientGithubLinkId], references: [id])
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade)
userId String? @db.Uuid // User ID if type is "referral"
echoAppId String? @db.Uuid // App ID if type is "markup"
recipientGithubLink GithubLink? @relation(fields: [recipientGithubLinkId], references: [id])
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade)

@@map("payouts")
}
Expand Down Expand Up @@ -302,34 +302,36 @@ enum EnumPaymentSource {
}

model Transaction {
id String @id @default(uuid()) @db.Uuid
transactionMetadataId String? @db.Uuid
totalCost Decimal @default(0.0) @db.Decimal(65, 14)
appProfit Decimal @default(0.0) @db.Decimal(65, 14)
markUpProfit Decimal @default(0.0) @db.Decimal(65, 14)
referralProfit Decimal @default(0.0) @db.Decimal(65, 14)
rawTransactionCost Decimal @default(0.0) @db.Decimal(65, 14)
status String?
isArchived Boolean @default(false)
archivedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
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])
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)
userSpendPoolUsage UserSpendPoolUsage? @relation(fields: [userSpendPoolUsageId], references: [id])
referralCode ReferralCode? @relation(fields: [referralCodeId], references: [id])
referrerReward ReferralReward? @relation(fields: [referrerRewardId], references: [id])
id String @id @default(uuid()) @db.Uuid
transactionMetadataId String? @db.Uuid
x402TransactionMetadataId String? @db.Uuid
totalCost Decimal @default(0.0) @db.Decimal(65, 14)
appProfit Decimal @default(0.0) @db.Decimal(65, 14)
markUpProfit Decimal @default(0.0) @db.Decimal(65, 14)
referralProfit Decimal @default(0.0) @db.Decimal(65, 14)
status String?
rawTransactionCost Decimal @default(0.0) @db.Decimal(65, 14)
isArchived Boolean @default(false)
archivedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
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])
markUp MarkUp? @relation(fields: [markUpId], references: [id])
spendPool SpendPool? @relation(fields: [spendPoolId], references: [id])
transactionMetadata TransactionMetadata? @relation(fields: [transactionMetadataId], references: [id])
x402TransactionMetadata x402TransactionMetadata? @relation(fields: [x402TransactionMetadataId], 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])

@@map("transactions")
}
Expand Down Expand Up @@ -372,26 +374,39 @@ model UserSpendPoolUsage {
}

model TransactionMetadata {
id String @id @default(uuid()) @db.Uuid
providerId String
provider String
model String
inputTokens Int?
outputTokens Int?
totalTokens Int?
id String @id @default(uuid()) @db.Uuid
providerId String
provider String
model String
inputTokens Int?
outputTokens Int?
totalTokens Int?
durationSeconds Int?
generateAudio Boolean?
toolCost Decimal @default(0.0) @db.Decimal(65, 14)
prompt String?
isArchived Boolean @default(false)
archivedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
transactions Transaction[]
generateAudio Boolean?
toolCost Decimal @default(0.0) @db.Decimal(65, 14)
prompt String?
isArchived Boolean @default(false)
archivedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
transactions Transaction[]

@@map("transaction_metadata")
}

model x402TransactionMetadata {
id String @id @default(uuid()) @db.Uuid
resourcePath String
resourceArgs Json @default("{}")
resourceResponse Json @default("{}")
resourceError Json @default("{}")
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz
transactions Transaction[]

@@map("x402_transaction_metadata")
}

model ReferralCode {
id String @id @default(uuid()) @db.Uuid
code String @unique
Expand Down Expand Up @@ -494,16 +509,17 @@ model OutboundEmailSent {
}

model VideoGenerationX402 {
videoId String @id
wallet String?
userId String? @db.Uuid
echoAppId String? @db.Uuid
cost Decimal
createdAt DateTime @default(now()) @db.Timestamptz(6)
expiresAt DateTime @db.Timestamptz(6)
isFinal Boolean @default(false)

user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
videoId String @id
wallet String?
userId String? @db.Uuid
echoAppId String? @db.Uuid
cost Decimal
createdAt DateTime @default(now()) @db.Timestamptz(6)
expiresAt DateTime @db.Timestamptz(6)
isFinal Boolean @default(false)

user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
echoApp EchoApp? @relation(fields: [echoAppId], references: [id], onDelete: Cascade)

@@map("video_generation_x402")
}
1 change: 1 addition & 0 deletions packages/app/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"uuid": "^11.1.0",
"viem": "^2.33.3",
"winston": "^3.17.0",
"x402": "^0.6.5",
"x402-express": "^0.6.5",
"zod": "^4.1.11"
},
Expand Down
16 changes: 16 additions & 0 deletions packages/app/server/src/errors/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,19 @@ export class UnknownModelError extends HttpError {
super(400, message);
}
}

export class MissingProxyError extends HttpError {
constructor(
message: string = 'Missing proxy parameter: Query must be passed with ?proxy="<proxy_url>"'
) {
super(400, message);
}
}

export class InvalidProxyError extends HttpError {
constructor(
message: string = 'Invalid proxy URL: Proxy must be a valid URL. Example: ?proxy="https://proxy.example.com"'
) {
super(400, message);
}
}
92 changes: 92 additions & 0 deletions packages/app/server/src/routers/x402-credit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Router } from 'express';
import { checkBalance } from 'services/BalanceCheckService';
import { Transaction, X402CreditHandlerInput } from 'types';
import { X402CreditRequestService } from 'services/HandleX402CreditService';
import { Decimal } from 'generated/prisma/runtime/library';
import logger from 'logger';
import { authenticateRequest } from 'auth';
import { EscrowRequest } from 'middleware/transaction-escrow-middleware';
import { prisma } from 'server';
import { Response as ExpressResponse } from 'express';
import { EchoDbService } from 'services/DbService';

const x402Router: Router = Router();

x402Router.post('/', async (req: EscrowRequest, res: ExpressResponse) => {
try {
const headers = req.headers as Record<string, string>;
const { processedHeaders, echoControlService } = await authenticateRequest(
headers,
prisma
);

const dbService = new EchoDbService(prisma);

return handleApiX402CreditRequest({
req,
res,
headers: processedHeaders,
echoControlService,
dbService,
});
} catch (error) {
logger.error('Failed to handle X402 credit request', error);
return res.status(500).json({ error: 'Internal server error' });
}
});

export async function handleApiX402CreditRequest({
req,
res,
headers,
echoControlService,
dbService,
}: X402CreditHandlerInput) {
try {
const handleX402CreditRequestService = new X402CreditRequestService({
req,
res,
headers,
echoControlService,
dbService,
});

const balanceCheckResult = await checkBalance(echoControlService);
const balanceCheckDecimal = new Decimal(
balanceCheckResult.effectiveBalance || 0
);

const x402RequestPrice =
await handleX402CreditRequestService.getX402RequestPrice();
if (x402RequestPrice.gt(balanceCheckDecimal)) {
return res.status(402).json({ error: 'Insufficient balance' });
}

const transaction: Transaction = {
metadata: {
resourcePath: req.body.resourcePath,
resourceArgs: req.body.resourceArgs,
resourceResponse: req.body.resourceResponse,
resourceError: req.body.resourceError,
},
rawTransactionCost: x402RequestPrice,
status: 'success',
};

const createdTransaction = await echoControlService.createTransaction(
transaction,
x402RequestPrice
);
const response =
await handleX402CreditRequestService.executeX402RequestAndUpdateMetadata(
createdTransaction
);

return res.status(response.status).json(response.body);
} catch (error) {
logger.error('Failed to handle X402 credit request', error);
return res.status(500).json({ error: 'Internal server error' });
}
}

export { x402Router };
3 changes: 3 additions & 0 deletions packages/app/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { initializeProvider } from './services/ProviderInitializationService';
import { getRequestMaxCost } from './services/PricingService';
import { Decimal } from '@prisma/client/runtime/library';
import resourceRouter from './routers/resource';
import { x402Router } from 'routers/x402-credit';

dotenv.config();

Expand Down Expand Up @@ -98,6 +99,8 @@ app.use(inFlightMonitorRouter);
// Use resource router for resource routes
app.use('/resource', resourceRouter);

app.use('/x402', x402Router);

// Main route handler
app.all('*', async (req: EscrowRequest, res: Response, next: NextFunction) => {
try {
Expand Down
Loading
Loading