Skip to content

feat: add x402 payment node for paid API calls#6389

Open
597226617 wants to merge 5 commits into
FlowiseAI:mainfrom
597226617:feat/x402-payment-node
Open

feat: add x402 payment node for paid API calls#6389
597226617 wants to merge 5 commits into
FlowiseAI:mainfrom
597226617:feat/x402-payment-node

Conversation

@597226617
Copy link
Copy Markdown

Summary

Adds an x402 Payment Node to Flowise that enables AI agent workflows to call paid external APIs using the x402 HTTP 402 payment protocol.

What it does

  1. Takes an endpoint URL, request params, and max price as input
  2. Calls the endpoint
  3. Handles HTTP 402 Payment Required responses
  4. Signs and sends USDC payment on Solana (SPL TransferChecked)
  5. Retries with X-Payment header
  6. Returns the paid API response + transaction metadata

x402 Protocol

x402 is an open payment protocol for AI agents — pay per API call with on-chain USDC. No API keys, no accounts, no subscriptions. Supported by the Linux Foundation.

Files Added

  • packages/components/nodes/tools/x402Payment/X402Payment.ts — Node UI definition
  • packages/components/nodes/tools/x402Payment/core.ts — Payment logic (x402 flow)
  • packages/components/nodes/tools/x402Payment/payment.svg — Node icon
  • packages/components/credentials/X402Wallet.credential.ts — Wallet credential

Testing

  • Tested with local development setup
  • Payment envelope generation verified against @acedatacloud/x402-client spec

Related

Add support for the x402 HTTP 402 payment protocol to Flowise:
- X402Wallet credential type for storing wallet private keys (Solana/Base)
- X402Payment tool that automatically handles 402 Payment Required responses
- Supports Solana USDC payments via TransferChecked instruction
- Supports Base (Ethereum L2) USDC payments
- Includes proper error handling, max price validation, and payment retries
- Added dependencies: @solana/web3.js, @solana/spl-token, ethers

The node enables AI agent workflows to call paid external APIs that
require payment via the x402 protocol.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new x402 payment node for Flowise, enabling AI agents to interact with paid APIs using the x402 protocol on Solana and Base networks. The implementation includes a new credential type for wallets and a tool that handles HTTP 402 responses by signing and sending USDC payments. Feedback highlights critical issues in the Solana transaction logic, such as missing blockhashes and fee payers, as well as potential failures due to hardcoded RPC URLs and private key format assumptions. Additionally, improvements were suggested regarding inconsistent return types and the enforcement of output length constraints during the payment flow.

Comment on lines +135 to +136
const transaction = new Transaction().add(instruction)
const signature = await connection.sendTransaction(transaction, [keypair])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The Solana transaction is being sent without a recentBlockhash or feePayer being explicitly set on the transaction object. While sendTransaction can sign the transaction, it does not automatically fetch a blockhash if it's missing, which will cause the transaction to fail on-chain. You should fetch the latest blockhash from the connection before sending.

        const { blockhash } = await connection.getLatestBlockhash();
        const transaction = new Transaction();
        transaction.add(instruction);
        transaction.recentBlockhash = blockhash;
        transaction.feePayer = fromAddress;
        const signature = await connection.sendTransaction(transaction, [keypair]);

throw new Error(`Unsupported currency: ${paymentReq.currency}`)
}

const keypair = Keypair.fromSecretKey(Buffer.from(this.privateKey, 'base64'))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Solana private keys (secret keys) are typically represented as Base58 strings or as a JSON array of numbers. Using Buffer.from(this.privateKey, 'base64') will fail for these standard formats. Validating the input format and throwing an error for invalid types promotes fail-fast behavior.

References
  1. When handling potentially invalid data from external sources, prefer throwing an error for invalid input types rather than silently returning a default or empty value.

}

const keypair = Keypair.fromSecretKey(Buffer.from(this.privateKey, 'base64'))
const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Solana and Base RPC URLs are hardcoded to public endpoints (lines 114 and 165). These public services often have strict rate limits and may be unreliable for production workloads. It is recommended to allow users to provide their own RPC URLs via the node parameters or credentials.

Comment on lines +259 to +271
const response: X402PaymentResponse = {
success: true,
data: text,
txHash: signature,
payment: {
amount: paymentReq.price,
currency: paymentReq.currency,
chain: paymentReq.chain,
txHash: signature
}
}

return JSON.stringify(response)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tool returns a JSON string when a payment is processed, but returns raw text when no payment is required. This inconsistent return format makes it difficult for downstream nodes to parse the output reliably. Avoid overloading the return structure with different data types to prevent fragile logic.

References
  1. Avoid overloading a single property to hold different types of data if it can lead to fragile logic or incorrect UI reconstruction.

const text = await res.text()
const response: X402PaymentResponse = {
success: true,
data: text,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The maxOutputLength constraint is ignored when a payment is required. The response data should be truncated to respect the user's configuration, similar to how it is handled in the non-payment path on line 279.

Suggested change
data: text,
data: text.slice(0, this.maxOutputLength),

孙备 and others added 2 commits May 15, 2026 16:14
…ication

- Remove connection.sendTransaction() from signSolanaPayment
- Remove contract.transfer() call from signBasePayment
- Both methods now only sign and serialize transactions locally
- Server receives signed transaction in X-Payment header for verification
- Prevents spending funds before API validates payment proof
- Remove CLAUDE.md from git tracking (accidentally committed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rice check

- Fixed Solana amount calculation to use BigInt instead of Math.floor for precision
- Changed 402 response parsing from HTTP headers to JSON body per x402 protocol
- Updated X402PaymentRequired interface to match x402 spec (maxAmountRequired, asset, network, scheme, extra)
- Fixed maxPrice validation to check maxAmountRequired instead of price
- Used getAssociatedTokenAddressSync to avoid unnecessary async RPC calls
- Removed memo field as it's not in x402 protocol spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@TateLyman
Copy link
Copy Markdown

Ran a no-payment protocol read-through of the current x402 node implementation. I did not send payment headers, sign requests, or attempt paid calls. A few merge-blocking points still look worth fixing before this goes into Flowise:

  1. The 402 parser expects top-level maxAmountRequired, asset, payTo, network, and scheme, but common x402 responses put one or more payment requirements under accepts[]. If the endpoint returns the standard body shape, parsePaymentRequired(await res.json()) will reject valid 402 challenges instead of selecting a compatible requirement.

  2. maxAmountRequired should be treated as an atomic-unit string, not a human USDC decimal. The current Solana path parses it as a number and then multiplies by 1e6 again before TransferChecked, which can double-scale the amount. The max-price check has the same unit mismatch because it compares atomic units directly to the user-facing USDC max.

  3. asset is normally the payment asset address/mint for the selected network, not the literal string usdc. The current paymentReq.asset !== "usdc" guard will reject many valid x402 challenges, including standard Base/Solana USDC requirements that identify the token by contract/mint.

  4. The Base path signs a raw ERC-20 transfer transaction, but the common x402 exact flow for EVM USDC is an EIP-3009 authorization payload that the resource server/facilitator verifies and settles. A raw transfer transaction envelope will not be accepted by standard x402 verifiers unless this node is intentionally targeting a custom server contract flow.

  5. The paid-response path still returns data: text without applying maxOutputLength, so a paid response can ignore the same output bound that the free path enforces.

Patch order I would use: normalize the challenge selection around accepts[], keep atomic units as bigint/string until formatting for display, convert the user max price into asset atomic units using selected requirement decimals, validate asset against known USDC mint/contract per network, and use the standard x402 payment payload builder for each supported scheme rather than hand-rolling incompatible envelopes.

孙备 and others added 2 commits May 17, 2026 14:04
…EIP-3009, asset validation)

- Parse accepts[] array from 402 response with V1 fallback compatibility
- Keep maxAmountRequired as atomic units (string/bigint) throughout
- Implement EIP-3009 transferWithAuthorization for Base payments
- Validate USDC mint addresses instead of string comparison
- Apply maxOutputLength truncation to paid responses
- Support multiple private key formats (base64, base58, JSON array)
- Make Solana and Base RPC URLs configurable
- Ensure consistent JSON return format for all responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@597226617
Copy link
Copy Markdown
Author

x402 V2 Protocol Compliance Fixes

Addressed all 5 merge-blocking issues from @TateLyman's review, plus 3 code quality improvements:

Merge-Blocking Fixes

  1. ✅ Parse `accepts[]` array — `parsePaymentRequired()` now checks for V2 `accepts[]` first, selects compatible requirement (Solana/Base), falls back to V1 top-level fields. Supports CAIP-2 network IDs.

  2. ✅ Atomic units throughout — `maxAmountRequired` is now `string` type, kept as atomic units (BigInt). V1 fallback converts decimal→atomic. User's `maxPrice` converted to atomic units for comparison.

  3. ✅ Asset validation — Validates against known USDC mint addresses per network (Solana mainnet/devnet, Base mainnet). No more `!== "usdc"` string comparison.

  4. ✅ EIP-3009 for EVM — Base path now uses `transferWithAuthorization` (EIP-712 typed data signing) instead of raw ERC-20 transfer. Signed authorization included in envelope, NOT broadcast — server/facilitator verifies and settles.

  5. ✅ `maxOutputLength` on paid responses — Both free and paid response paths now apply output truncation.

Code Quality Fixes

  1. Private key format — Supports base64, base58 (bs58), and JSON array formats with clear error messages
  2. Configurable RPCs — `solanaRpcUrl` and `baseRpcUrl` parameters added
  3. Consistent JSON returns — All paths return JSON (including non-payment responses)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants