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
10 changes: 7 additions & 3 deletions packages/app/server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const env = createEnv({
// Server
PORT: z.coerce.number().default(3069),
NODE_ENV: z
.enum(['development', 'production', 'staging', "test"])
.enum(['development', 'production', 'staging', 'test'])
.default('development'),

// Network & Blockchain
Expand Down Expand Up @@ -50,8 +50,12 @@ export const env = createEnv({
GOOGLE_SERVICE_ACCOUNT_KEY_ENCODED: z.string().optional(),

// API Keys - Echo
API_KEY_HASH_SECRET: z.string().default('change-this-in-production-very-secret-key'),
API_ECHO_ACCESS_JWT_SECRET: z.string().default('api-jwt-secret-change-in-production'),
API_KEY_HASH_SECRET: z
.string()
.default('change-this-in-production-very-secret-key'),
API_ECHO_ACCESS_JWT_SECRET: z
.string()
.default('api-jwt-secret-change-in-production'),

// Echo Configuration
ECHO_MARKUP: z.string().default('1.25'),
Expand Down
19 changes: 19 additions & 0 deletions packages/app/server/src/handlers/settle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export async function settle(
try {
xPaymentData = validateXPaymentHeader(headers, req);
} catch (error) {
logger.error('Payment header validation failed in settle', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
url: req.url,
method: req.method,
});
buildX402Response(req, res, maxCost);
return undefined;
}
Expand Down Expand Up @@ -92,6 +98,19 @@ export async function settle(
const settleResult = await facilitatorClient.settle(settleRequest);

if (!settleResult.success || !settleResult.transaction) {
logger.error('Payment settlement failed', {
errorReason: settleResult.errorReason,
payer: settleResult.payer,
paymentAmount: paymentAmount,
nonce: payload.authorization.nonce,
signature: payload.signature.substring(0, 20) + '...', // Log first 20 chars for debugging
from: payload.authorization.from,
to: payload.authorization.to,
validAfter: payload.authorization.validAfter,
validBefore: payload.authorization.validBefore,
url: req.url,
method: req.method,
});
buildX402Response(req, res, maxCost);
return undefined;
}
Expand Down
56 changes: 54 additions & 2 deletions packages/app/server/src/services/facilitator/facilitatorProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
PaymentRequirements,
SettleResponse,
VerifyResponse,
ExactEvmPayload,
} from './x402-types';
import logger, { logMetric } from '../../logger';
import dotenv from 'dotenv';
Expand Down Expand Up @@ -64,12 +65,10 @@ export async function facilitatorProxy<
payload: PaymentPayload,
paymentRequirements: PaymentRequirements
): Promise<T> {

if (!PROXY_FACILITATOR_URL) {
throw new Error('PROXY_FACILITATOR_URL is not set');
}


logMetric('facilitator_proxy_attempt', 1, {
method,
});
Expand All @@ -96,6 +95,59 @@ export async function facilitatorProxy<
);

if (res.status !== 200) {
let errorBody: string | undefined;
try {
errorBody = await res.text();
} catch (e) {
// Ignore errors reading response body
}

// Extract payment details for logging
const paymentDetails: Record<string, any> = {
scheme: payload.scheme,
network: payload.network,
x402Version: payload.x402Version,
};

// Extract signature and nonce if it's an ExactEvmPayload
if (
'payload' in payload &&
payload.payload &&
typeof payload.payload === 'object'
) {
const evmPayload = payload.payload as ExactEvmPayload;
if (
'signature' in evmPayload &&
typeof evmPayload.signature === 'string'
) {
paymentDetails.signature =
evmPayload.signature.substring(0, 20) + '...';
}
if (
'authorization' in evmPayload &&
evmPayload.authorization &&
typeof evmPayload.authorization === 'object'
) {
if ('nonce' in evmPayload.authorization) {
paymentDetails.nonce = evmPayload.authorization.nonce;
}
if ('from' in evmPayload.authorization) {
paymentDetails.from = evmPayload.authorization.from;
}
if ('to' in evmPayload.authorization) {
paymentDetails.to = evmPayload.authorization.to;
}
}
}

logger.error('Facilitator proxy returned non-200 status', {
method,
status: res.status,
statusText: res.statusText,
errorBody: errorBody?.substring(0, 1000), // Limit to first 1000 chars
paymentPayload: paymentDetails,
});

logMetric('facilitator_proxy_failure', 1, {
method,
status: res.status,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ type TokenAmount = string;
type Url = string;
type Nonce = string;

interface ExactEvmPayloadAuthorization {
export interface ExactEvmPayloadAuthorization {
from: Address;
to: Address;
value: TokenAmount;
Expand Down
Loading