Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/app/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"neverthrow": "^7.2.0",
"@coinbase/cdp-sdk": "^1.34.0",
"@e2b/code-interpreter": "^2.0.1",
"@google-cloud/storage": "^7.17.1",
Expand Down
33 changes: 33 additions & 0 deletions packages/app/server/src/errors/results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Typed error variants for neverthrow Result types.
* Each error variant carries structured context for downstream handling.
*/

export type DbError =
| { type: 'DB_NOT_FOUND'; entity: string; id?: string }
| { type: 'DB_VALIDATION_FAILED'; message: string }
| { type: 'DB_TRANSACTION_FAILED'; cause: unknown }
| { type: 'DB_QUERY_FAILED'; cause: unknown };

export type AuthError =
| { type: 'AUTH_INVALID_API_KEY' }
| { type: 'AUTH_EXPIRED_JWT' }
| { type: 'AUTH_JWT_VERIFICATION_FAILED'; cause: unknown }
| { type: 'AUTH_MISSING_FIELDS'; fields: string[] }
| { type: 'AUTH_MISSING_CREDENTIALS' };

export type SettleError =
| { type: 'SETTLE_SMART_ACCOUNT_FAILED'; cause: unknown }
| { type: 'SETTLE_INVALID_PAYMENT_HEADER'; cause: unknown }
| { type: 'SETTLE_INVALID_PAYLOAD'; cause: unknown }
| { type: 'SETTLE_INSUFFICIENT_PAYMENT'; required: bigint; provided: bigint }
| { type: 'SETTLE_FACILITATOR_FAILED' };

export type RefundError =
| { type: 'REFUND_TRANSFER_FAILED'; cause: unknown };

export type ResourceError =
| { type: 'RESOURCE_EXECUTION_FAILED'; cause: unknown }
| { type: 'RESOURCE_AUTHENTICATION_FAILED'; cause: unknown }
| { type: 'RESOURCE_PAYMENT_FAILED'; cause: SettleError }
| { type: 'RESOURCE_TRANSACTION_FAILED'; cause: unknown };
22 changes: 14 additions & 8 deletions packages/app/server/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TransactionEscrowMiddleware } from 'middleware/transaction-escrow-middleware';
import { modelRequestService } from 'services/ModelRequestService';
import { ApiKeyHandlerInput, X402HandlerInput } from 'types';
import { calculateRefundAmount } from 'utils';
import { calculateRefundAmount, buildX402Response } from 'utils';
import { checkBalance } from 'services/BalanceCheckService';
import { prisma } from 'server';
import { makeProxyPassthroughRequest } from 'services/ProxyPassthroughService';
Expand All @@ -25,7 +25,7 @@ export async function handleX402Request({
return await makeProxyPassthroughRequest(req, res, provider, headers);
}

const settlePromise = settle(req, res, headers, maxCost);
const settlePromise = settle(req, headers, maxCost);

const modelResultPromise = modelRequestService
.executeModelRequest(req, res, headers, provider, isStream)
Expand All @@ -37,13 +37,16 @@ export async function handleX402Request({
modelResultPromise,
]);

const settleOk = settleResult.isOk();

// Case 1: Settle failed and model failed
if (!settleResult && !modelResult.success) {
if (!settleOk && !modelResult.success) {
buildX402Response(req, res, maxCost);
return;
}

// Case 2: Settle failed but model succeeded
if (!settleResult && modelResult.success) {
if (!settleOk && modelResult.success) {
const { data } = modelResult;
logger.error('Settle failed but model request succeeded', {
provider: provider.getType(),
Expand All @@ -62,16 +65,19 @@ export async function handleX402Request({
return;
}

// At this point, settleResult is guaranteed to exist
if (!settleResult) {
// At this point, settleResult is guaranteed to be ok
if (settleResult.isErr()) {
buildX402Response(req, res, maxCost);
return;
}

const { payload, paymentAmountDecimal } = settleResult;
const { payload, paymentAmountDecimal } = settleResult.value;

// Case 3: Settle succeeded but model failed
if (!modelResult.success) {
await refund(paymentAmountDecimal, payload);
refund(paymentAmountDecimal, payload).mapErr(refundErr => {
logger.error('Failed to refund', refundErr);
});
return;
}

Expand Down
21 changes: 11 additions & 10 deletions packages/app/server/src/handlers/refund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import { decimalToUsdcBigInt } from 'utils';
import { transfer } from 'transferWithAuth';
import { ExactEvmPayload } from 'services/facilitator/x402-types';
import { Decimal } from '@prisma/client/runtime/library';
import logger from 'logger';
import { ResultAsync } from 'neverthrow';
import type { RefundError } from '../errors/results';

export async function refund(
export function refund(
paymentAmountDecimal: Decimal,
payload: ExactEvmPayload
) {
try {
const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal);
const authPayload = payload.authorization;
await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt);
} catch (error) {
logger.error('Failed to refund', error);
}
): ResultAsync<void, RefundError> {
const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal);
const authPayload = payload.authorization;

return ResultAsync.fromPromise(
transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt),
(cause): RefundError => ({ type: 'REFUND_TRANSFER_FAILED', cause })
).map(() => undefined);
}
155 changes: 85 additions & 70 deletions packages/app/server/src/handlers/settle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
usdcBigIntToDecimal,
decimalToUsdcBigInt,
buildX402Response,
getSmartAccount,
validateXPaymentHeader,
} from 'utils';
Expand All @@ -10,91 +9,107 @@ import { FacilitatorClient } from 'services/facilitator/facilitatorService';
import {
ExactEvmPayload,
ExactEvmPayloadSchema,
PaymentPayload,
PaymentRequirementsSchema,
SettleRequestSchema,
Network,
} from 'services/facilitator/x402-types';
import { Decimal } from '@prisma/client/runtime/library';
import logger from 'logger';
import { Request, Response } from 'express';
import { Request } from 'express';
import { ResultAsync, fromThrowable, err, ok } from 'neverthrow';
import { env } from '../env';
import type { SettleError } from '../errors/results';

export async function settle(
export type SettleSuccess = {
payload: ExactEvmPayload;
paymentAmountDecimal: Decimal;
};

const parseXPaymentHeader = fromThrowable(
validateXPaymentHeader,
(cause): SettleError => ({ type: 'SETTLE_INVALID_PAYMENT_HEADER', cause })
);

export function settle(
req: Request,
res: Response,
headers: Record<string, string>,
maxCost: Decimal
): Promise<
{ payload: ExactEvmPayload; paymentAmountDecimal: Decimal } | undefined
> {
): ResultAsync<SettleSuccess, SettleError> {
const network = env.NETWORK as Network;

let recipient: string;
try {
recipient = (await getSmartAccount()).smartAccount.address;
} catch (error) {
buildX402Response(req, res, maxCost);
return undefined;
}
return ResultAsync.fromPromise(
getSmartAccount().then(({ smartAccount }) => smartAccount.address),
(cause): SettleError => ({ type: 'SETTLE_SMART_ACCOUNT_FAILED', cause })
)
.andThen(recipient =>
parseXPaymentHeader(headers, req).map(xPaymentData => ({
recipient,
xPaymentData,
}))
)
.andThen(({ recipient, xPaymentData }) => {
const payloadResult = ExactEvmPayloadSchema.safeParse(xPaymentData.payload);
if (!payloadResult.success) {
logger.error('Invalid ExactEvmPayload in settle', {
error: payloadResult.error,
payload: xPaymentData.payload,
});
return err<
{ recipient: string; xPaymentData: typeof xPaymentData; payload: ExactEvmPayload; paymentAmountDecimal: Decimal },
SettleError
>({ type: 'SETTLE_INVALID_PAYLOAD', cause: payloadResult.error });
}

let xPaymentData: PaymentPayload;
try {
xPaymentData = validateXPaymentHeader(headers, req);
} catch (error) {
buildX402Response(req, res, maxCost);
return undefined;
}

const payloadResult = ExactEvmPayloadSchema.safeParse(xPaymentData.payload);
if (!payloadResult.success) {
logger.error('Invalid ExactEvmPayload in settle', {
error: payloadResult.error,
payload: xPaymentData.payload,
});
buildX402Response(req, res, maxCost);
return undefined;
}
const payload = payloadResult.data;
const payload = payloadResult.data;
const paymentAmount = payload.authorization.value;
const paymentAmountDecimal = usdcBigIntToDecimal(paymentAmount);

const paymentAmount = payload.authorization.value;
const paymentAmountDecimal = usdcBigIntToDecimal(paymentAmount);
// Note(shafu, alvaro): Edge case where client sends the x402-challenge
// but the payment amount is less than what we returned in the first response
if (BigInt(paymentAmount) < decimalToUsdcBigInt(maxCost)) {
return err<
{ recipient: string; xPaymentData: typeof xPaymentData; payload: ExactEvmPayload; paymentAmountDecimal: Decimal },
SettleError
>({
type: 'SETTLE_INSUFFICIENT_PAYMENT',
required: decimalToUsdcBigInt(maxCost),
provided: BigInt(paymentAmount),
});
}

// Note(shafu, alvaro): Edge case where client sends the x402-challenge
// but the payment amount is less than what we returned in the first response
if (BigInt(paymentAmount) < decimalToUsdcBigInt(maxCost)) {
buildX402Response(req, res, maxCost);
return undefined;
}
return ok({ recipient, xPaymentData, payload, paymentAmountDecimal });
})
.andThen(({ recipient, xPaymentData, payload, paymentAmountDecimal }) => {
const facilitatorClient = new FacilitatorClient();
const paymentRequirements = PaymentRequirementsSchema.parse({
scheme: 'exact',
network,
maxAmountRequired: payload.authorization.value,
resource: `${req.protocol}://${req.get('host')}${req.url}`,
description: 'Echo x402',
mimeType: 'application/json',
payTo: recipient,
maxTimeoutSeconds: 60,
asset: USDC_ADDRESS,
extra: {
name: 'USD Coin',
version: '2',
},
});

const facilitatorClient = new FacilitatorClient();
const paymentRequirements = PaymentRequirementsSchema.parse({
scheme: 'exact',
network,
maxAmountRequired: paymentAmount,
resource: `${req.protocol}://${req.get('host')}${req.url}`,
description: 'Echo x402',
mimeType: 'application/json',
payTo: recipient,
maxTimeoutSeconds: 60,
asset: USDC_ADDRESS,
extra: {
name: 'USD Coin',
version: '2',
},
});
const settleRequest = SettleRequestSchema.parse({
paymentPayload: xPaymentData,
paymentRequirements,
});

const settleRequest = SettleRequestSchema.parse({
paymentPayload: xPaymentData,
paymentRequirements,
});

const settleResult = await facilitatorClient.settle(settleRequest);

if (!settleResult.success || !settleResult.transaction) {
buildX402Response(req, res, maxCost);
return undefined;
}

return { payload, paymentAmountDecimal };
return ResultAsync.fromPromise(
facilitatorClient.settle(settleRequest),
(): SettleError => ({ type: 'SETTLE_FACILITATOR_FAILED' })
).andThen(settleResult => {
if (!settleResult.success || !settleResult.transaction) {
return err<SettleSuccess, SettleError>({ type: 'SETTLE_FACILITATOR_FAILED' });
}
return ok<SettleSuccess, SettleError>({ payload, paymentAmountDecimal });
});
});
}
Loading