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
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/inference-facilitator.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 65 additions & 31 deletions all_networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import {
type PaymentSettlementJobData,
type SettlementApiJobResponse,
} from "./all_networks_types_helpers.js";
import { closeInferenceUsageTracker, getInferenceUsageStats } from "./all_networks_usage.js";
import {
closeInferenceUsageTracker,
getInferenceUsageStats,
recordInferenceUsage,
} from "./all_networks_usage.js";
import {
type PaymentPayload,
type PaymentRequirements,
Expand Down Expand Up @@ -188,32 +192,44 @@ async function toJobResponse(
return response;
}

async function enqueuePaymentSettlementJob(args: {
paymentPayload: PaymentPayload;
paymentRequirements: PaymentRequirements;
usageMetadata?: InferenceUsageMetadata;
}): Promise<SettlementApiJobResponse> {
const paymentJobId = `payment-${randomUUID()}`;
const paymentJob = await paymentQueue.add(
"payment-settlement",
{
paymentPayload: args.paymentPayload,
paymentRequirements: args.paymentRequirements,
usageMetadata: args.usageMetadata,
},
{
jobId: paymentJobId,
},
);

console.log("[api] Payment settlement job enqueued", {
jobId: paymentJobId,
hasUsageMetadata: Boolean(args.usageMetadata),
...summarizePaymentRequirements(args.paymentRequirements),
...summarizePaymentPayload(args.paymentPayload),
});
function paymentMetricTags(paymentRequirements: PaymentRequirements): string[] {
const tagValue = (value: string | undefined, fallback = "unknown") => {
const normalized = value?.trim().toLowerCase();
if (!normalized) {
return fallback;
}
return normalized.replace(/[^a-z0-9_.:-]+/g, "_").slice(0, 100) || fallback;
};
return [
"worker:payment",
`network:${tagValue(paymentRequirements.network)}`,
`asset:${tagValue(paymentRequirements.asset)}`,
`scheme:${tagValue(paymentRequirements.scheme)}`,
];
}

return toJobResponse(PAYMENT_QUEUE_NAME, paymentJob);
function recordPaymentSettlementMetrics(
paymentRequirements: PaymentRequirements,
result: { success: boolean; errorReason?: string; amount?: string },
): void {
const tags = paymentMetricTags(paymentRequirements);
if (result.success) {
incrementMetric("payment.settled.count", tags);
const amount = Number(paymentRequirements.amount);
if (Number.isFinite(amount)) {
incrementMetric("payment.settled.amount", tags, amount);
}
return;
}
const reason =
typeof result.errorReason === "string"
? result.errorReason.split(":")[0].replace(/[^a-z0-9_.:-]+/gi, "_").toLowerCase()
: "unknown";
const failureTags = [...tags, `reason:${reason || "unknown"}`];
incrementMetric("payment.settlement.failed.count", failureTags);
if (reason.startsWith("invalid_exact_evm")) {
incrementMetric("payment.settlement.invalid_evm.count", failureTags);
}
}

async function enqueueDataSettlementJob(args: {
Expand Down Expand Up @@ -344,12 +360,30 @@ app.post("/settle", async (req, res) => {
});
debugLog("[api][debug] /settle request body", req.body);

const paymentJob = await enqueuePaymentSettlementJob({
paymentPayload,
paymentRequirements,
usageMetadata,
// x402 clients must receive the concrete settlement result in this HTTP
// response so they can commit the voucher/channel state. Inference usage
// accounting remains asynchronous below; it never blocks the x402 receipt.
const result = await facilitator.settle(paymentPayload, paymentRequirements);
recordPaymentSettlementMetrics(paymentRequirements, result);

if (result.success && usageMetadata) {
const usageSessionId = usageMetadata.sessionId ?? `payment-${randomUUID()}`;
void recordInferenceUsage(
{ ...usageMetadata, sessionId: usageSessionId },
result.payer,
).catch(error => {
console.error("[api] Failed to record inference usage", summarizeError(error));
});
}

console.log("[api] /settle request completed", {
...summarizePaymentRequirements(paymentRequirements),
success: result.success,
transaction: result.transaction,
errorReason: result.success ? undefined : result.errorReason,
});
return res.status(202).json({ paymentJob });
debugLog("[api][debug] /settle response", result);
return res.json(result);
} catch (error) {
console.error("[api] /settle request failed", summarizeError(error));
if (isSettlementError(error)) {
Expand Down
28 changes: 25 additions & 3 deletions all_networks_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
createErc20ApprovalGasSponsoringExtension,
type Erc20ApprovalGasSponsoringSigner,
} from "@x402/extensions";
import { toFacilitatorEvmSigner, type FacilitatorEvmSigner } from "@x402/evm";
import { toFacilitatorEvmSigner, type AuthorizerSigner, type FacilitatorEvmSigner } from "@x402/evm";
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/facilitator";
import { ExactEvmScheme } from "@x402/evm/exact/facilitator";
import { UptoEvmScheme } from "@x402/evm/upto/facilitator";
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
Expand Down Expand Up @@ -1277,6 +1278,21 @@ export async function createFacilitator(): Promise<x402Facilitator> {
const evmAccount = privateKeyToAccount(evmPrivateKey);
console.info(`EVM Facilitator account: ${evmAccount.address}`);

// Batch settlement needs a receiver authorizer to sign claim/refund EIP-712
// messages. A dedicated key can be configured; using the facilitator key as
// the fallback preserves the existing single-key deployment configuration.
const receiverAuthorizerPrivateKey = (process.env.BATCH_SETTLEMENT_RECEIVER_AUTHORIZER_PRIVATE_KEY ||
evmPrivateKey) as `0x${string}`;
const receiverAuthorizerAccount = privateKeyToAccount(receiverAuthorizerPrivateKey);
const receiverAuthorizerSigner: AuthorizerSigner = {
address: receiverAuthorizerAccount.address,
signTypedData: params =>
receiverAuthorizerAccount.signTypedData(
params as Parameters<typeof receiverAuthorizerAccount.signTypedData>[0],
),
};
console.info(`Batch receiver authorizer: ${receiverAuthorizerSigner.address}`);

const viemClient = createWalletClient({
account: evmAccount,
chain: ogEvm,
Expand Down Expand Up @@ -1310,11 +1326,12 @@ export async function createFacilitator(): Promise<x402Facilitator> {
message: Record<string, unknown>;
signature: `0x${string}`;
}) => viemClient.verifyTypedData(args as never),
writeContract: (args: {
writeContract: ({ dataSuffix: _dataSuffix, ...args }: {
address: `0x${string}`;
abi: readonly unknown[];
functionName: string;
args: readonly unknown[];
dataSuffix?: `0x${string}`;
}) =>
viemClient.writeContract({
...args,
Expand Down Expand Up @@ -1353,11 +1370,12 @@ export async function createFacilitator(): Promise<x402Facilitator> {
message: Record<string, unknown>;
signature: `0x${string}`;
}) => baseViemClient.verifyTypedData(args as never),
writeContract: (args: {
writeContract: ({ dataSuffix: _dataSuffix, ...args }: {
address: `0x${string}`;
abi: readonly unknown[];
functionName: string;
args: readonly unknown[];
dataSuffix?: `0x${string}`;
}) =>
baseViemClient.writeContract({
...args,
Expand Down Expand Up @@ -1403,6 +1421,10 @@ export async function createFacilitator(): Promise<x402Facilitator> {
new ExactEvmScheme(baseEvmSigner, { deployERC4337WithEIP6492: true }),
);
facilitator.register(BASE_MAINNET_NETWORK, new UptoEvmScheme(baseEvmSigner));
facilitator.register(
BASE_MAINNET_NETWORK,
new BatchSettlementEvmScheme(baseEvmSigner, receiverAuthorizerSigner),
);

facilitator
.registerExtension(EIP2612_GAS_SPONSORING)
Expand Down
Loading