
Research
TeamPCP Compromises Telnyx Python SDK to Deliver Credential-Stealing Malware
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.
@x402/extensions
Advanced tools
x402 Payment Protocol Extensions. This package provides optional extensions that enhance the x402 payment protocol with additional functionality.
pnpm install @x402/extensions
Extensions are optional features that can be added to x402 payment flows. They follow a standardized { info, schema } structure and are included in PaymentRequired.extensions and PaymentPayload.extensions.
This package includes:
The Bazaar Discovery Extension enables facilitators to automatically catalog and index x402-enabled resources by following server-declared discovery instructions. This allows users to discover paid APIs and services through facilitator catalogs.
"GET /weather")Declare endpoint discovery metadata in your payment middleware configuration. This helps facilitators understand how to call your endpoints and what they return.
Note: The HTTP method is automatically inferred from the route key (e.g.,
"GET /weather"→ GET method). You don't need to specify it indeclareDiscoveryExtension.
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
const resources = {
"GET /weather": {
accepts: {
scheme: "exact",
price: "$0.001",
network: "eip155:84532",
payTo: "0xYourAddress"
},
extensions: {
...declareDiscoveryExtension({
input: { city: "San Francisco" },
inputSchema: {
properties: {
city: { type: "string" },
units: { type: "string", enum: ["celsius", "fahrenheit"] }
},
required: ["city"]
},
output: {
example: {
city: "San Francisco",
weather: "foggy",
temperature: 15,
humidity: 85
}
},
}),
},
},
};
For POST, PUT, and PATCH endpoints, specify bodyType to indicate the request body format:
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
const resources = {
"POST /api/translate": {
accepts: {
scheme: "exact",
price: "$0.01",
network: "eip155:84532",
payTo: "0xYourAddress"
},
extensions: {
...declareDiscoveryExtension({
input: {
text: "Hello, world!",
targetLanguage: "es"
},
inputSchema: {
properties: {
text: { type: "string" },
targetLanguage: { type: "string", pattern: "^[a-z]{2}$" }
},
required: ["text", "targetLanguage"]
},
bodyType: "json",
output: {
example: {
translatedText: "¡Hola, mundo!",
sourceLanguage: "en",
targetLanguage: "es"
}
},
}),
},
},
};
const resources = {
"PUT /api/user/profile": {
accepts: {
scheme: "exact",
price: "$0.05",
network: "eip155:84532",
payTo: "0xYourAddress"
},
extensions: {
...declareDiscoveryExtension({
input: {
name: "John Doe",
email: "john@example.com",
bio: "Software developer"
},
inputSchema: {
properties: {
name: { type: "string", minLength: 1 },
email: { type: "string", format: "email" },
bio: { type: "string", maxLength: 500 }
},
required: ["name", "email"]
},
bodyType: "form-data",
output: {
example: {
success: true,
userId: "123",
updatedAt: "2024-01-01T00:00:00Z"
}
},
}),
},
},
};
const resources = {
"DELETE /api/data/:id": {
accepts: {
scheme: "exact",
price: "$0.001",
network: "eip155:84532",
payTo: "0xYourAddress"
},
extensions: {
...declareDiscoveryExtension({
input: { id: "123" },
inputSchema: {
properties: {
id: { type: "string" }
},
required: ["id"]
},
output: {
example: {
success: true,
deletedId: "123"
}
},
}),
},
},
};
For MCP (Model Context Protocol) tools, use the toolName field instead of bodyType/input. The HTTP method is not relevant -- MCP tools are invoked by name.
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
const resources = {
"POST /mcp": {
accepts: {
scheme: "exact",
price: "$0.01",
network: "eip155:84532",
payTo: "0xYourAddress"
},
extensions: {
...declareDiscoveryExtension({
toolName: "financial_analysis",
description: "Analyze financial data for a given ticker",
inputSchema: {
type: "object",
properties: {
ticker: { type: "string", description: "Stock ticker symbol" },
analysis_type: {
type: "string",
enum: ["fundamental", "technical", "sentiment"],
},
},
required: ["ticker"],
},
example: { ticker: "AAPL", analysis_type: "fundamental" },
output: {
example: {
pe_ratio: 28.5,
recommendation: "hold",
confidence: 0.85
}
},
}),
},
},
};
You can optionally specify transport to indicate the MCP transport type ("streamable-http" or "sse"). When omitted, streamable-http is assumed per the MCP spec.
import { paymentProxy, x402ResourceServer } from "@x402/next";
import { HTTPFacilitatorClient } from "@x402/core/http";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { declareDiscoveryExtension } from "@x402/extensions/bazaar";
const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" });
const resourceServer = new x402ResourceServer(facilitatorClient)
.register("eip155:84532", new ExactEvmScheme());
export const proxy = paymentProxy(
{
"/api/weather": {
accepts: {
scheme: "exact",
price: "$0.001",
network: "eip155:84532",
payTo: "0xYourAddress",
},
extensions: {
...declareDiscoveryExtension({
input: { city: "San Francisco" },
inputSchema: {
properties: { city: { type: "string" } },
required: ["city"],
},
output: {
example: { city: "San Francisco", weather: "foggy" }
},
}),
},
},
},
resourceServer,
);
Extract discovery information from incoming payment requests to catalog resources in the Bazaar.
import { extractDiscoveryInfo } from "@x402/extensions/bazaar";
import type { PaymentPayload, PaymentRequirements } from "@x402/core/types";
async function handlePayment(
paymentPayload: PaymentPayload,
paymentRequirements: PaymentRequirements
) {
// Extract discovery info from the payment
const discovered = extractDiscoveryInfo(paymentPayload, paymentRequirements);
if (discovered) {
// discovered contains:
// {
// resourceUrl: "https://api.example.com/weather",
// method: "GET",
// x402Version: 2,
// discoveryInfo: {
// input: { type: "http", method: "GET", queryParams: { city: "..." } },
// output: { type: "json", example: { ... } }
// }
// }
// Catalog the resource in your Bazaar
await catalogResource({
url: discovered.resourceUrl,
method: discovered.method,
inputSchema: discovered.discoveryInfo.input,
outputExample: discovered.discoveryInfo.output?.example,
});
}
}
import { validateDiscoveryExtension, extractDiscoveryInfo } from "@x402/extensions/bazaar";
function processPayment(paymentPayload: PaymentPayload, paymentRequirements: PaymentRequirements) {
const discovered = extractDiscoveryInfo(paymentPayload, paymentRequirements);
if (discovered && paymentPayload.extensions?.bazaar) {
// Validate the extension schema
const validation = validateDiscoveryExtension(paymentPayload.extensions.bazaar);
if (!validation.valid) {
console.warn("Invalid discovery extension:", validation.errors);
// Handle invalid extension (log, reject, etc.)
return;
}
// Extension is valid, proceed with cataloging
catalogResource(discovered);
}
}
The bazaarResourceServerExtension automatically enriches discovery extensions with HTTP method information from the request context:
import { bazaarResourceServerExtension } from "@x402/extensions/bazaar";
import { x402ResourceServer } from "@x402/core/server";
// The extension helper automatically extracts discovery info
const resourceServer = new x402ResourceServer(facilitatorClient)
.register("eip155:84532", new ExactEvmScheme())
.registerExtension(bazaarResourceServerExtension);
declareDiscoveryExtension(config)Creates a discovery extension object for resource servers. Accepts either an HTTP endpoint config or an MCP tool config.
HTTP Parameters:
config.input (optional): Example input values (query params for GET/HEAD/DELETE, body for POST/PUT/PATCH)config.inputSchema (optional): JSON Schema for input validationconfig.bodyType (required for body methods): For POST/PUT/PATCH, specify "json", "form-data", or "text". This is how TypeScript discriminates between query methods (GET/HEAD/DELETE) and body methods.config.output (optional): Output specification
output.example: Example output dataoutput.schema: JSON Schema for output validationNote: The HTTP method is NOT passed to this function. It is automatically inferred from the route key (e.g.,
"GET /weather") or enriched bybazaarResourceServerExtensionat runtime.
MCP Parameters:
config.toolName (required): MCP tool name — the presence of this field identifies the config as MCPconfig.description (optional): Human-readable tool descriptionconfig.inputSchema (required): JSON Schema for tool argumentsconfig.example (optional): Example tool argumentsconfig.transport (optional): MCP transport type ("streamable-http" or "sse"). Defaults to streamable-http per the MCP spec when omitted.config.output (optional): Output specification
output.example: Example output dataoutput.schema: JSON Schema for output validationReturns: An object with a bazaar key containing the discovery extension.
Examples:
// HTTP endpoint
const httpExtension = declareDiscoveryExtension({
input: { query: "search term" },
inputSchema: {
properties: { query: { type: "string" } },
required: ["query"]
},
output: {
example: { results: [] }
}
});
// MCP tool
const mcpExtension = declareDiscoveryExtension({
toolName: "search",
description: "Search for documents",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"]
},
output: {
example: { results: [] }
}
});
// Both return: { bazaar: { info: {...}, schema: {...} } }
extractDiscoveryInfo(paymentPayload, paymentRequirements, validate?)Extracts discovery information from a payment request (for facilitators).
Parameters:
paymentPayload: The payment payload from the clientpaymentRequirements: The payment requirements from the servervalidate (optional): Whether to validate the extension (default: true)Returns: DiscoveredResource object or null if not found.
interface DiscoveredHTTPResource {
resourceUrl: string;
method: string; // e.g. "GET", "POST"
x402Version: number;
discoveryInfo: DiscoveryInfo;
}
interface DiscoveredMCPResource {
resourceUrl: string;
toolName: string; // MCP tool name
x402Version: number;
discoveryInfo: DiscoveryInfo;
}
type DiscoveredResource = DiscoveredHTTPResource | DiscoveredMCPResource;
validateDiscoveryExtension(extension)Validates a discovery extension's info against its schema.
Returns: { valid: boolean, errors?: string[] }
validateAndExtract(extension)Validates and extracts discovery info in one step.
Returns: { valid: boolean, info?: DiscoveryInfo, errors?: string[] }
bazaarResourceServerExtensionA server extension that automatically enriches HTTP discovery extensions with method information from the request context. MCP extensions are passed through unchanged.
Usage:
import { bazaarResourceServerExtension } from "@x402/extensions/bazaar";
const resourceServer = new x402ResourceServer(facilitatorClient)
.registerExtension(bazaarResourceServerExtension);
BAZAARThe extension identifier constant ("bazaar").
The Sign-In-With-X extension implements CAIP-122 for chain-agnostic wallet authentication. It allows clients to prove control of a wallet that previously paid for a resource, enabling access without repurchase.
sign-in-with-x extension containing challenge parametersSIGN-IN-WITH-X headerimport {
declareSIWxExtension,
siwxResourceServerExtension,
createSIWxSettleHook,
createSIWxRequestHook,
InMemorySIWxStorage,
} from '@x402/extensions/sign-in-with-x';
// Storage for tracking paid addresses
const storage = new InMemorySIWxStorage();
// 1. Register extension for time-based field refreshment
const resourceServer = new x402ResourceServer(facilitatorClient)
.register(NETWORK, new ExactEvmScheme())
.registerExtension(siwxResourceServerExtension) // Refreshes nonce/timestamps per request
.onAfterSettle(createSIWxSettleHook({ storage })); // Records payments
// 2. Declare SIWX support in routes (network/domain/uri derived automatically)
const routes = {
"GET /data": {
accepts: [{scheme: "exact", price: "$0.01", network: "eip155:8453", payTo}],
extensions: declareSIWxExtension({
statement: 'Sign in to access your purchased content',
}),
},
};
// 3. Verify incoming SIWX proofs
const httpServer = new x402HTTPResourceServer(resourceServer, routes)
.onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access if paid
// Optional: Enable smart wallet support (EIP-1271/EIP-6492)
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const publicClient = createPublicClient({ chain: base, transport: http() });
const httpServerWithSmartWallets = new x402HTTPResourceServer(resourceServer, routes)
.onProtectedRequest(createSIWxRequestHook({
storage,
verifyOptions: { evmVerifier: publicClient.verifyMessage },
}));
The hooks automatically:
network from accepts, domain/uri from request URL, refreshes nonce/issuedAt/expirationTime per requestimport {
declareSIWxExtension,
parseSIWxHeader,
validateSIWxMessage,
verifySIWxSignature,
SIGN_IN_WITH_X,
} from '@x402/extensions/sign-in-with-x';
// 1. Declare in PaymentRequired response
const extensions = {
[SIGN_IN_WITH_X]: declareSIWxExtension({
domain: 'api.example.com',
resourceUri: 'https://api.example.com/data',
network: 'eip155:8453',
statement: 'Sign in to access your purchased content',
}),
};
// 2. Verify incoming proof
async function handleRequest(request: Request) {
const header = request.headers.get('SIGN-IN-WITH-X');
if (!header) return; // No auth provided
// Parse the header
const payload = parseSIWxHeader(header);
// Validate message fields (expiry, nonce, domain, etc.)
const validation = await validateSIWxMessage(
payload,
'https://api.example.com/data'
);
if (!validation.valid) {
return { error: validation.error };
}
// Verify signature and recover address
const verification = await verifySIWxSignature(payload);
if (!verification.valid) {
return { error: verification.error };
}
// verification.address is the verified wallet
// Check if this wallet has paid before
const hasPaid = await checkPaymentHistory(verification.address);
if (hasPaid) {
// Grant access without payment
}
}
import { createSIWxClientHook } from '@x402/extensions/sign-in-with-x';
import { x402HTTPClient } from '@x402/fetch';
// Configure client with SIWX hook - automatically tries SIWX auth before payment
const httpClient = new x402HTTPClient(client)
.onPaymentRequired(createSIWxClientHook(signer));
// Requests automatically use SIWX auth when server supports it
const response = await httpClient.fetch(url);
The client hook automatically:
supportedChainsimport {
createSIWxPayload,
encodeSIWxHeader,
} from '@x402/extensions/sign-in-with-x';
// 1. Get extension and network from 402 response
const paymentRequired = await response.json();
const extension = paymentRequired.extensions['sign-in-with-x'];
const paymentNetwork = paymentRequired.accepts[0].network; // e.g., "eip155:8453"
// 2. Find matching chain in supportedChains
const matchingChain = extension.supportedChains.find(
chain => chain.chainId === paymentNetwork
);
if (!matchingChain) {
// Payment network not supported for SIWX
throw new Error('Chain not supported');
}
// 3. Build complete info with selected chain
const completeInfo = {
...extension.info,
chainId: matchingChain.chainId,
type: matchingChain.type,
};
// 4. Create signed payload
const payload = await createSIWxPayload(completeInfo, signer);
// 5. Encode and send
const header = encodeSIWxHeader(payload);
const response = await fetch(url, {
headers: { 'SIGN-IN-WITH-X': header }
});
declareSIWxExtension(options?)Creates the extension object for servers to include in PaymentRequired. Most fields are derived automatically from request context when using siwxResourceServerExtension.
declareSIWxExtension({
// All fields optional - derived from context if omitted
domain?: string; // Server domain (derived from request URL)
resourceUri?: string; // Full resource URI (derived from request URL)
network?: string | string[]; // CAIP-2 network(s) (derived from accepts[].network)
statement?: string; // Human-readable purpose
version?: string; // CAIP-122 version (default: "1")
expirationSeconds?: number; // Challenge TTL in seconds
})
Automatic derivation: When using siwxResourceServerExtension, omitted fields are derived:
network → from accepts[].network in route configresourceUri → from request URLdomain → parsed from resourceUriMulti-chain support: When network is an array (or multiple networks in accepts), supportedChains will contain one entry per network.
parseSIWxHeader(header)Parses a base64-encoded SIGN-IN-WITH-X header into a payload object.
validateSIWxMessage(payload, resourceUri, options?)Validates message fields (expiry, domain binding, nonce, etc.).
validateSIWxMessage(payload, resourceUri, {
maxAge?: number; // Max age for issuedAt (default: 5 min)
checkNonce?: (nonce) => boolean; // Custom nonce validation
})
// Returns: { valid: boolean; error?: string }
verifySIWxSignature(payload, options?)Verifies the cryptographic signature and recovers the signer address.
verifySIWxSignature(payload, {
evmVerifier?: EVMMessageVerifier; // For smart wallet support
})
// Returns: { valid: boolean; address?: string; error?: string }
Smart Wallet Support (EIP-1271 / EIP-6492):
By default, only EOA (Externally Owned Account) signatures are verified. To support smart contract wallets (like Coinbase Smart Wallet, Safe, etc.), pass publicClient.verifyMessage from viem:
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const publicClient = createPublicClient({
chain: base,
transport: http()
});
// In your request hook
const result = await verifySIWxSignature(payload, {
evmVerifier: publicClient.verifyMessage,
});
This enables:
Note: Smart wallet verification requires RPC calls, while EOA verification is purely local.
createSIWxPayload(serverInfo, signer)Client helper that creates and signs a complete payload.
encodeSIWxHeader(payload)Encodes a payload as base64 for the SIGN-IN-WITH-X header.
SIGN_IN_WITH_XExtension identifier constant ("sign-in-with-x").
| Scheme | Description |
|---|---|
eip191 | personal_sign (default for EVM EOAs) |
eip1271 | Smart contract wallet verification |
eip6492 | Counterfactual smart wallet verification |
siws | Sign-In-With-Solana |
Problem: extractDiscoveryInfo returns null.
Solutions:
declareDiscoveryExtensionpaymentPayload.extensions.bazaar existsoutputSchema)Problem: validateDiscoveryExtension returns valid: false.
Solutions:
inputSchema matches the structure of inputinputSchema.requiredProblem: verifySIWxSignature returns valid: false.
Solutions:
checkSmartWallet option with a providerProblem: validateSIWxMessage returns valid: false.
Solutions:
issuedAt is recent (within maxAge, default 5 minutes)expirationTime hasn't passeddomain matches the server's domainuri matches the resource URIThis package supports both x402 v1 and v2:
PaymentPayload.extensions and PaymentRequired.extensionsPaymentRequirements.outputSchema (automatically converted)The extractDiscoveryInfo function automatically handles both versions.
FAQs
x402 Payment Protocol Extensions
The npm package @x402/extensions receives a total of 20,187 weekly downloads. As such, @x402/extensions popularity was classified as popular.
We found that @x402/extensions demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.

Security News
/Research
Widespread GitHub phishing campaign uses fake Visual Studio Code security alerts in Discussions to trick developers into visiting malicious website.