Skip to content

Commit 0aa3de1

Browse files
MQ37jirispilkaclaude
authored
feat: introduce PaymentProvider interface and migrate Skyfire to provider pattern, add x402 payment provider (#590)
* feat: introduce PaymentProvider interface and migrate Skyfire to provider pattern Replace hardcoded Skyfire payment logic with a generic PaymentProvider interface that supports multiple payment schemes (Skyfire, x402). This refactoring: - Add src/payments/ module with PaymentProvider type, SkyfirePaymentProvider, and resolver - Replace skyfireMode boolean with paymentProvider instance on server options - Rename requiresSkyfirePayId to paymentRequired on tool definitions - Rename createApifyClientWithSkyfireSupport to createApifyClientWithPaymentSupport - Replace skyfirePayId with generic paymentHeaders on ApifyClient - Preserve full backward compatibility in index_internals.ts exports * chore: remove dead validateSkyfirePayId utility The function is superseded by SkyfirePaymentProvider.validatePayment() and has no remaining callers. * refactor: address PR review - rename methods, add preparePayment helper, pass client via InternalToolArgs - Rename PaymentProvider methods: augmentTool→decorateToolSchema, stripPaymentArgs→removePaymentFields, redactArgs→redactForLogging - Add preparePayment() helper centralizing payment processing (cleanArgs, logArgs, client creation) - Move ApifyClient creation to server handler, pass via InternalToolArgs instead of per-tool creation - Remove additionalProperties:true hack from common tools (no longer needed since payment fields are stripped before validation) - Restore additionalProperties:true for call-actor and dynamic Actor tools (needed for arbitrary Actor input) - Remove stale Skyfire-specific comments from tool files - Add PaymentHeaders type alias * refactor: rename payments/prepare.ts to payments/helpers.ts * feat: add x-apify-payment-protocol header to Skyfire payment headers for consistency * fix: Replace stale Skyfire-specific comments with provider-agnostic wording (#605) * fix: replace stale Skyfire-specific comments with provider-agnostic wording Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix: replace Skyfire-specific error messages with provider-agnostic wording --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: MQ37 <themq37@gmail.com> * feat: implement X402PaymentProvider for x402 protocol support (#591) * feat: implement X402PaymentProvider for x402 protocol support Add X402PaymentProvider that reads payment signatures from MCP _meta["x402/payment"] and forwards them as PAYMENT-SIGNATURE headers to the Apify API. - Add src/payments/x402.ts with full PaymentProvider implementation - Add PaymentMeta type and meta? parameter to validatePayment/getPaymentHeaders - Thread meta through InternalToolArgs, server.ts, and all 14 tool files - Wire up x402 in resolvePaymentProvider (replaces placeholder error) - Export X402PaymentProvider and PaymentMeta from barrel * feat: extract x402 payment signature from HTTP header with _meta fallback - Add HTTP PAYMENT-SIGNATURE header extraction as fallback when _meta is absent - Thread requestHeaders through PaymentProvider interface (validatePayment, getPaymentHeaders) - Prefer _meta over HTTP header for backward compatibility - Fix isError: true on payment validation error responses - Add 5 unit tests for x402 header extraction and precedence - Add x402 integration test for streamable-http transport - Replace skyfireMode boolean with generic payment string param in test helpers - Add typecheck script alias in package.json * feat: add x-apify-payment-protocol header to x402 payment headers * feat: finalize x402 payment support — propagate 402 errors to MCP clients (#608) * feat: finalize x402 payment support — propagate 402 errors to MCP clients - Intercept HTTP 402 from Apify API via Axios response interceptor - Extract payment-required header and throw structured McpError(402) - Fetch x402 payment requirements from API during server init - Inject x402 payment details into tool _meta.x402 via decorateToolSchema - Make resolvePaymentProvider async for x402 requirement fetching * refactor: use x402 tool result approach instead of JSON-RPC 402 errors Switch payment-required responses from McpError(402) throws to tool results with isError: true + content[0].text containing PaymentRequired JSON. This matches the x402 MCP transport spec used by @x402/mcp client/server. Changes: - Return buildMCPResponse with JSON-stringified PaymentRequired instead of throwing McpError(402) in all 3 code paths (validation, catch, task) - Do not use structuredContent (MCP SDK validates it against outputSchema) - Flatten first accepts entry in _meta.x402 via getFirstAcceptEntry() - Add try-catch guard on axios interceptor registration - Add instanceof ApifyApiError guard on extractPaymentRequiredData Source 2 - Add 8s AbortController timeout to fetchX402PaymentRequirements * fix: handle 402 payment required in task mode and update stale comments - Add 402 Payment Required handling to the task mode catch block, returning a proper x402 tool result so clients can auto-pay. - Do not use structuredContent in task mode 402 responses to avoid schema validation errors. - Update stale comments in types.ts, x402.ts, and payment_errors.ts to reflect the new tool result approach instead of McpError.data. * refactor: minimize x402 payment error handling and deduplicate logic - Centralized MCP response building for 402 errors in buildPaymentRequiredResponse helper. - Flattened the axios interceptor registration in registerPaymentRequiredInterceptor to use optional chaining instead of deeply nested try/catch blocks. - Modified preparePayment to return a pre-built errorResult instead of individual error strings/data, saving 30 lines of repeated response logic in server.ts. - Deduplicated signature extraction in X402PaymentProvider by adding getEncodedPaymentSignature helper. * fix(payments): address PR #608 review comments - Add 30m promise caching to `fetchX402PaymentRequirements()` to prevent blocking SSE setup - Deduplicate Skyfire log redaction using shared `redactSkyfirePayId` (see #618 for migration plan) - Add `create()` factory to `SkyfirePaymentProvider` to normalize instantiation - Generalize Actor MCP restriction messages to apply to any third-party payment provider * fix(lint): remove trailing whitespace from x402.ts comments --------- Co-authored-by: Jiří Spilka <jiri.spilka@apify.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 36de0a4 commit 0aa3de1

43 files changed

Lines changed: 926 additions & 255 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"lint": "eslint .",
9393
"lint:fix": "eslint . --fix",
9494
"type-check": "tsc -p tsconfig.json --noEmit",
95+
"typecheck": "npm run type-check",
9596
"check": "npm run type-check && npm run lint",
9697
"check:widgets": "tsx scripts/check_widgets.ts",
9798
"test": "npm run test:unit",

src/actor/server.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { parseBooleanOrNull } from '@apify/utilities';
1616

1717
import { ApifyClient } from '../apify_client.js';
1818
import { ActorsMcpServer } from '../mcp/server.js';
19+
import { resolvePaymentProvider } from '../payments/index.js';
1920
import type { ApifyRequestParams } from '../types.js';
2021
import { parseUiMode } from '../types.js';
2122
import { getHelpMessage, Routes, TransportType } from './const.js';
@@ -67,9 +68,8 @@ export function createExpressApp(
6768

6869
const uiMode = parseUiMode(urlParams.get('ui')) ?? parseUiMode(process.env.UI_MODE);
6970

70-
// Extract payment mode parameter - if payment=skyfire, enable skyfire mode
71-
const paymentParam = urlParams.get('payment');
72-
const skyfireMode = paymentParam === 'skyfire';
71+
// Resolve payment provider from URL parameter (e.g., ?payment=skyfire)
72+
const paymentProvider = await resolvePaymentProvider(urlParams.get('payment'));
7373

7474
const mcpServer = new ActorsMcpServer({
7575
taskStore,
@@ -79,7 +79,7 @@ export function createExpressApp(
7979
enabled: telemetryEnabled,
8080
},
8181
uiMode,
82-
skyfireMode,
82+
paymentProvider,
8383
});
8484
const transport = new SSEServerTransport(Routes.MESSAGE, res);
8585

@@ -199,9 +199,8 @@ export function createExpressApp(
199199

200200
const uiMode = parseUiMode(urlParams.get('ui')) ?? parseUiMode(process.env.UI_MODE);
201201

202-
// Extract payment mode parameter - if payment=skyfire, enable skyfire mode
203-
const paymentParam = urlParams.get('payment');
204-
const skyfireMode = paymentParam === 'skyfire';
202+
// Resolve payment provider from URL parameter (e.g., ?payment=skyfire)
203+
const paymentProvider = await resolvePaymentProvider(urlParams.get('payment'));
205204

206205
const mcpServer = new ActorsMcpServer({
207206
taskStore,
@@ -212,7 +211,7 @@ export function createExpressApp(
212211
enabled: telemetryEnabled,
213212
},
214213
uiMode,
215-
skyfireMode,
214+
paymentProvider,
216215
});
217216

218217
// Load MCP server tools

src/apify_client.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { ApifyClient as _ApifyClient } from 'apify-client';
33
import type { AxiosRequestConfig } from 'axios';
44

55
import { USER_AGENT_ORIGIN } from './const.js';
6-
import type { ActorsMcpServer } from './mcp/server.js';
7-
import type { ApifyToken } from './types.js';
6+
import type { PaymentHeaders } from './payments/types.js';
87

98
type ExtendedApifyClientOptions = Omit<ApifyClientOptions, 'token'> & {
109
token?: string | null | undefined;
11-
skyfirePayId?: string;
10+
/** Payment headers to forward on outbound API requests (from PaymentProvider.getPaymentHeaders) */
11+
paymentHeaders?: PaymentHeaders;
1212
};
1313

1414
/**
@@ -42,16 +42,16 @@ export class ApifyClient extends _ApifyClient {
4242
delete options.token;
4343
}
4444

45-
const { skyfirePayId, ...clientOptions } = options;
45+
const { paymentHeaders, ...clientOptions } = options;
4646
const requestInterceptors = [addUserAgent];
4747
/**
48-
* Add skyfire-pay-id header if provided.
48+
* Add payment headers if provided by a PaymentProvider.
4949
*/
50-
if (skyfirePayId) {
50+
if (paymentHeaders && Object.keys(paymentHeaders).length > 0) {
5151
requestInterceptors.push((config) => {
5252
const updatedConfig = { ...config };
5353
updatedConfig.headers = updatedConfig.headers ?? {};
54-
updatedConfig.headers['skyfire-pay-id'] = skyfirePayId;
54+
Object.assign(updatedConfig.headers, paymentHeaders);
5555
return updatedConfig;
5656
});
5757
}
@@ -64,22 +64,3 @@ export class ApifyClient extends _ApifyClient {
6464
});
6565
}
6666
}
67-
68-
/**
69-
* Creates ApifyClient with appropriate credentials based on Skyfire mode.
70-
* In Skyfire mode, uses skyfire-pay-id from args; otherwise uses apifyToken.
71-
*
72-
* @param apifyMcpServer - The MCP server instance with configuration options
73-
* @param args - Tool arguments that may contain skyfire-pay-id
74-
* @param apifyToken - Standard Apify token for non-Skyfire mode
75-
* @returns ApifyClient instance configured for the appropriate mode
76-
*/
77-
export function createApifyClientWithSkyfireSupport(
78-
apifyMcpServer: ActorsMcpServer,
79-
args: Record<string, unknown>,
80-
apifyToken: ApifyToken,
81-
): ApifyClient {
82-
return apifyMcpServer.options.skyfireMode && typeof args['skyfire-pay-id'] === 'string'
83-
? new ApifyClient({ skyfirePayId: args['skyfire-pay-id'] })
84-
: new ApifyClient({ token: apifyToken });
85-
}

src/const.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,8 @@ export const TOOL_STATUS = {
200200
SOFT_FAIL: 'SOFT_FAIL',
201201
} as const;
202202

203+
// HTTP status codes
204+
export const HTTP_PAYMENT_REQUIRED = 402;
205+
203206
// Modes that allow long running task tool executions
204207
export const ALLOWED_TASK_TOOL_EXECUTION_MODES = ['optional', 'required'] as const;

src/index_internals.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import { ApifyClient } from './apify_client.js';
66
import { APIFY_FAVICON_URL, defaults, HelperTools, SERVER_NAME, SERVER_TITLE } from './const.js';
77
import { processParamsGetTools } from './mcp/utils.js';
8+
import { resolvePaymentProvider } from './payments/index.js';
9+
import type { PaymentProvider } from './payments/types.js';
810
import { getServerCard } from './server_card.js';
911
import { addTool } from './tools/common/add_actor.js';
1012
import { getActorsAsTools, getCategoryTools, getDefaultTools, getUnauthEnabledToolCategories,
@@ -48,5 +50,10 @@ export {
4850
readJsonFile,
4951
parseCommaSeparatedList,
5052
parseQueryParamList,
53+
resolvePaymentProvider,
54+
type PaymentProvider,
55+
/**
56+
* @deprecated Use the server's paymentProvider.redactForLogging instead. This will be removed in a future release.
57+
*/
5158
redactSkyfirePayId,
5259
};

0 commit comments

Comments
 (0)