This workflow corresponds to n8n.io template #7206 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "fVQBtrZ0NyAMHS3D",
"name": "n8n Gateway for 1Shot Gas Station",
"tags": [],
"nodes": [
{
"id": "80932427-585a-4ca3-8ee2-cf8aaf756aae",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1888,
512
],
"parameters": {
"width": 408,
"height": 524,
"content": "## x402 Payment Endpoint \n\nThis workflow lets the operator monetize swaps from ERC-20 tokens to native gas tokens on up to 100 different EVM blockchains via the [Li.Fi](https://li.fi/) protocol. \n\nThis workflow accepts an [x402 payment](https://www.x402.org/) header that will be used with the [1Shot Gas Station](https://github.com/uxlySoftware/1Shot-Gas-Station) smart contract which interacts with Li.Fi to swap EIP-3009 compatible tokens for native tokens (even cross-chain) without the user paying gas. \n\n1. Click on the webhook trigger and give the route a good name.\n2. Click on the \"Payment Token Configs\" and set up the desired tokens you want to support."
},
"typeVersion": 1
},
{
"id": "39a72092-252c-4e96-9ecb-0b271da73cd5",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2176,
1088
],
"parameters": {
"width": 840,
"height": 596,
"content": "## Example Curl Command\n\nThe workflow requires the following POST body parameters:\n\n- `fromChain` - the chain the user's EIP-3009 compatible tokens are on\n- `fromToken` - the address of the EIP-3009 compatible token to swap for native token\n- `fromAmount` - the amount of `fromToken` to swap\n- `fromAddress` - the user's address that holds the `fromToken`, must be the same as the `from` address in the `x-payment` header\n- `toChain` - the chain id of the target network to receive native token on\n\nYou can test [1Shot Gas Station](https://github.com/uxlysoftware/1shot-gas-station?tab=readme-ov-file#1shot-api-gas-station) endpoint with a command like this (be sure to use a [properly formatted x-payment header](https://1shotapi.com/tools) payload: \n\n```sh\n# swap out the URL here for you webhook URL endpoint\ncurl -X POST \\\n https://n8n.1shotapi.dev/webhook-test/refactored-gas-station \\\n -H \"x-payment: YOUR-BASE64-ENCODED-PAYMENT-PAYLOAD\" \\\n -H \"User-Agent: CustomUserAgent/1.0\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"fromChain\": \"8453\",\n \"fromToken\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\n \"fromAmount\": \"100000000\",\n \"fromAddress\": \"0x55680c6b69d598c0b42f93cd53dff3d20e069b5b\",\n \"toChain\": \"43114\"\n }'\n```"
},
"typeVersion": 1
},
{
"id": "3e805ca4-f2f2-43db-9f50-ccffd5b770d2",
"name": "Response: 200 - Payment Successful",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-64,
416
],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "text",
"responseBody": "={ \"transactionHash\": \"{{ $json.transactionHash }}\" }"
},
"typeVersion": 1.3
},
{
"id": "2708bdf9-fcbc-4d0b-b2c1-dbb1ba24e35f",
"name": "1Shot API Submit & Wait",
"type": "n8n-nodes-1shot.oneShotSynch",
"position": [
-288,
512
],
"parameters": {
"params": "={\n \"tokenAddress\": \"{{ $('Webhook').item.json.body.fromToken }}\",\n \"from\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.from }}\",\n \"value\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.value }}\",\n \"validAfter\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.validAfter }}\",\n \"validBefore\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.validBefore }}\",\n \"nonce\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.nonce }}\",\n \"v\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.v }}\",\n \"r\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.r }}\",\n \"s\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.s }}\",\n \"diamondCalldata\": \"{{ $('Fetch Li.Fi Quote').item.json.transactionRequest.data }}\"\n} ",
"additionalFields": {
"memo": "=Li.Fi swap - receiver: {{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.from }}, token: {{ $('Webhook').item.json.body.fromToken }},amount: {{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.value }}, chain: {{ $('Webhook').item.json.body.fromChain }}",
"gasLimit": "={{ (parseInt($('Fetch Li.Fi Quote').item.json.transactionRequest.gasLimit, 16) + 100000).toString() }}"
},
"contractMethodId": "={{ $('Payment Token Configs').item.json.paymentTokens[$('Validate & Verify POST Body').item.json.selectedToken].contractMethodId }}"
},
"credentials": {
"oneShotOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "f7e3314b-5cab-4ca7-930e-d00b03dbc5b4",
"name": "On Successful Payment Simulation",
"type": "n8n-nodes-base.if",
"position": [
-512,
608
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "81c67679-e256-4fd2-bed7-8f4272c2392b",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.success.toString() }}",
"rightValue": "true"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "acfea100-cd3a-4099-b828-c6fe0bcfc3ba",
"name": "Simulate Payment",
"type": "n8n-nodes-1shot.oneShot",
"onError": "continueRegularOutput",
"position": [
-736,
608
],
"parameters": {
"params": "={\n \"tokenAddress\": \"{{ $('Webhook').item.json.body.fromToken }}\",\n \"from\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.from }}\",\n \"value\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.value }}\",\n \"validAfter\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.validAfter }}\",\n \"validBefore\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.validBefore }}\",\n \"nonce\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.nonce }}\",\n \"v\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.v }}\",\n \"r\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.r }}\",\n \"s\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.s }}\",\n \"diamondCalldata\": \"{{ $('Fetch Li.Fi Quote').item.json.transactionRequest.data }}\"\n} ",
"gasLimit": "={{ (parseInt($('Fetch Li.Fi Quote').item.json.transactionRequest.gasLimit, 16) + 100000).toString() }}",
"operation": "simulate",
"contractMethodId": "={{ $('Payment Token Configs').item.json.paymentTokens[$('Validate & Verify POST Body').item.json.selectedToken].contractMethodId }}"
},
"credentials": {
"oneShotOAuth2Api": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "f5089506-8709-4942-9062-ac66fe091bbe",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1856,
896
],
"parameters": {
"path": "=refactored-gas-station",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "bd5ce8db-39eb-4194-ab5f-cffc73eb9397",
"name": "Fetch Li.Fi Quote",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"position": [
-1184,
800
],
"parameters": {
"url": "https://li.quest/v1/quote",
"options": {},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "fromChain",
"value": "={{ $('Webhook').item.json.body.fromChain }}"
},
{
"name": "fromToken",
"value": "={{ $('Webhook').item.json.body.fromToken }}"
},
{
"name": "fromAddress",
"value": "={{ $('Webhook').item.json.body.fromAddress }}"
},
{
"name": "toChain",
"value": "={{ $('Webhook').item.json.body.toChain }}"
},
{
"name": "toToken",
"value": "=0x0000000000000000000000000000000000000000"
},
{
"name": "toAddress",
"value": "={{ $('Webhook').item.json.body.fromAddress }}"
},
{
"name": "slippage",
"value": "0.05"
},
{
"name": "integrator",
"value": "oneshot-gas-station"
},
{
"name": "=fromAmount",
"value": "={{ $('Webhook').item.json.body.fromAmount }}"
},
{
"name": "allowBridges",
"value": "gasZipBridge"
},
{
"name": "fee",
"value": "0.005"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "x-lifi-api-key"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "a6e7de73-8e9c-49fb-b172-47e98695de17",
"name": "Response: Missing or Invalid Request Body Params",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-1184,
992
],
"parameters": {
"options": {
"responseCode": 402
},
"respondWith": "json",
"responseBody": "={\n \"x402Version\": \"1\",\n \"error\": \"{{ $('Validate & Verify POST Body').item.json.error.errorMessage }}\",\n \"accepts\": {{ JSON.stringify($('Validate & Verify POST Body').item.json.error.paymentConfigs) }}\n}"
},
"typeVersion": 1.3
},
{
"id": "b226d9e6-8963-4556-a6be-16b98486a326",
"name": "Response: Failed to Get Quote",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-960,
896
],
"parameters": {
"options": {
"responseCode": 402
},
"respondWith": "json",
"responseBody": "={\n \"x402Version\": \"1\",\n \"error\": {{ JSON.stringify($json.error.message) }},\n \"accepts\": {{ JSON.stringify($('Validate & Verify POST Body').item.json.accepts) }}\n}"
},
"typeVersion": 1.3
},
{
"id": "1f92afa5-ae85-41ce-9d2d-b868a37d5a03",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1280,
384
],
"parameters": {
"color": 5,
"width": 272,
"height": 576,
"content": "## Fetch Swap Route & Quote form Li.Fi\n\nThis block fetches a quote from the Li.Fi Route API that retuns the `callData` needed to call the Li.FI Diamond Proxy contract.\n\nYou'll need to get a Li.Fi API key so you don't get rate limited and put it in the Li.Fi node for the `x-lifi-api-key`.\n\nAdditionally, if you want to collect integrator fees, you'll need to change the `integrator` to point at your Li.Fi integrator account."
},
"typeVersion": 1
},
{
"id": "466fed72-b4fa-4f50-ab71-e3701d2e88a8",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-784,
352
],
"parameters": {
"color": 6,
"width": 672,
"height": 416,
"content": "## Use 1Shot API to Simulate and Submit the Swap \n\n[1Shot API](https://1shotapi.com) handles simulating the transaction and submitting it the the appropriate blockchain network. "
},
"typeVersion": 1
},
{
"id": "fd5beb2b-9129-4d89-8ec2-9e20c1d303a4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1264,
1184
],
"parameters": {
"width": 752,
"height": 352,
"content": "## Payment Token Configs\n\nYou must edit the \"Payment Token Configs\" node with the tokens you wish to support for swaps.\n\nYou need:\n\n1. The token address\n2. The EVM chain id\n3. The x402 name of the `toChain` (like `base` or `avalanche`)\n3. The token `name`\n4. Token `version`\n5. The `contractMethodId` of the `callDiamondWithEIP3009SignatureToNative` method that you've imported into your 1Shot API account on the same chain as the token. "
},
"typeVersion": 1
},
{
"id": "fefb995e-d5ee-4a4f-99ee-6e2c90d08d9d",
"name": "Validate & Verify X-Payment Header",
"type": "n8n-nodes-base.code",
"onError": "continueErrorOutput",
"position": [
-960,
704
],
"parameters": {
"jsCode": "const payTo = $('Payment Token Configs').first().json.gasStationContract;\nconst timeOut = $('Payment Token Configs').first().json.timeOut;\nconst paymentTokens = $('Payment Token Configs').first().json.paymentTokens;\nconst selectedToken = $('Validate & Verify POST Body').first().json.selectedToken;\nconst gasLimit = $('Fetch Li.Fi Quote').first().json.transactionRequest.gasLimit;\nconst estimate = {\n swapFee: $('Fetch Li.Fi Quote').first().json.estimate.feeCosts[0].amountUSD,\n gasCostUSD: $('Fetch Li.Fi Quote').first().json.estimate.gasCosts[0].amountUSD,\n toAmountUSD: $('Fetch Li.Fi Quote').first().json.estimate.toAmountUSD,\n toAmountETH: $('Fetch Li.Fi Quote').first().json.estimate.toAmount,\n gasEstimateETH: $('Fetch Li.Fi Quote').first().json.estimate.gasCosts[0].estimate,\n waitTime: $('Fetch Li.Fi Quote').first().json.estimate.executionDuration\n}\n\n\n// this function will split the signature from the user into r,s & v components\nfunction splitSignature(sigHex) {\n // Remove \"0x\" prefix if present\n const hex = sigHex.startsWith(\"0x\") ? sigHex.slice(2) : sigHex;\n\n // Convert hex to byte array\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n }\n\n if (bytes.length !== 65) {\n throw new Error(`Invalid signature length: got ${bytes.length} bytes`);\n }\n\n // Convert byte ranges to hex strings\n const toHex = (arr) =>\n \"0x\" + Array.from(arr).map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n const r = toHex(bytes.slice(0, 32));\n const s = toHex(bytes.slice(32, 64));\n\n // Normalize v\n let v = bytes[64];\n if (v === 0) v = 27;\n else if (v === 1) v = 28;\n else if (v !== 27 && v !== 28 && v >= 35) {\n v = (v & 1) ? 27 : 28; // EIP-155\n }\n\n const vHex = \"0x\" + v.toString(16).padStart(2, \"0\");\n\n return { r, s, v: vHex };\n}\n\n// this will make sure our x-payment header contains all necessary components\nfunction validateXPayment(obj) {\n // Define the expected structure and types\n const requiredShape = {\n x402Version: \"number\",\n scheme: \"string\",\n network: \"string\",\n payload: {\n authorization: {\n from: \"string\",\n to: \"string\",\n value: \"string\",\n validAfter: \"string\",\n validBefore: \"string\",\n nonce: \"string\"\n },\n signature: \"string\"\n }\n };\n\n let missing = [];\n function checkShape(expected, actual, path) {\n for (const key in expected) {\n const currentPath = path ? path + \".\" + key : key;\n\n if (!(key in actual)) {\n missing.push(\"Missing field: \" + currentPath);\n } else if (typeof expected[key] === \"object\") {\n if (typeof actual[key] !== \"object\" || actual[key] === null) {\n missing.push(\"Invalid type at \" + currentPath + \": expected object\");\n } else {\n checkShape(expected[key], actual[key], currentPath);\n }\n } else {\n if (typeof actual[key] !== expected[key]) {\n missing.push(\n \"Invalid type at \" + currentPath +\n \": expected \" + expected[key] +\n \", got \" + typeof actual[key]\n );\n }\n }\n }\n }\n\n checkShape(requiredShape, obj, \"\");\n if (missing.length > 0) {\n return missing.join(\"; \");\n }\n return \"valid\";\n}\n\n// this function will ensure the x-payment header is for one of our supported\n// networks, is for the correct amount, and pays the right address\nfunction verifyPaymentDetails(obj, config, payTo) {\n const errors = [];\n\n // 1. Check that network exists in config\n const network = obj.network;\n const configEntry = Object.values(config).find(\n (entry) => entry.network.toLowerCase() === (network || \"\").toLowerCase()\n );\n\n if (!configEntry) {\n errors.push(\"Invalid or unsupported network: \" + network);\n }\n\n // 2. Check value >= maxAmountRequired\n if (configEntry) {\n const required = BigInt(configEntry.maxAmountRequired);\n let actual;\n\n try {\n actual = BigInt(obj.payload.authorization.value);\n } catch (e) {\n errors.push(\"Invalid value: must be numeric string\");\n }\n\n if (typeof actual !== \"undefined\" && actual < required) {\n errors.push(\n `Value too low: got ${actual}, requires at least ${required}`\n );\n }\n }\n\n // 3. Check 'to' matches payTo (case-insensitive)\n const toAddr = obj.payload?.authorization?.to;\n if (!toAddr) {\n errors.push(\"Missing 'to' field in authorization\");\n } else if (toAddr.toLowerCase() !== payTo.toLowerCase()) {\n errors.push(`Invalid 'to' address: expected ${payTo}, got ${toAddr}`);\n }\n\n return errors.length > 0 ? errors.join(\"; \") : \"valid\";\n}\n\n\n\n// try to decode the x-payment header if it exists\ntry {\n // Decode the x-payment header from base64\n const xPaymentHeader = $('Webhook').first().json.headers['x-payment'];\n const decodedXPayment = Buffer.from(xPaymentHeader, 'base64').toString('utf-8');\n // Parse the decoded value into a JSON object\n const decodedXPaymentJson = JSON.parse(decodedXPayment);\n\n const validation = validateXPayment(decodedXPaymentJson)\n\n if (validation !== \"valid\") {\n return { \n error: {\n errorMessage: JSON.stringify(validation), \n paymentConfigs: [$('Validate & Verify POST Body').first().json.accepts[selectedToken]]\n } \n };\n }\n\n const verification = verifyPaymentDetails(decodedXPaymentJson, [paymentTokens[selectedToken]], payTo[paymentTokens[selectedToken].chain]);\n\n if (verification !== \"valid\") {\n return {\n error: {\n errorMessage: JSON.stringify(verification),\n paymentConfigs: [$('Validate & Verify POST Body').first().json.accepts[selectedToken]]\n }\n }\n }\n\n // make sure the quote amount is the same as the authorization amount or the tx will revert\n if (BigInt(decodedXPaymentJson[\"payload\"][\"authorization\"][\"value\"]) !== BigInt($('Payment Token Configs').first().json.body.fromAmount)) {\n return {\n error: {\n errorMessage: JSON.stringify(`fromAmount (${$('Payment Token Configs').first().json.body.fromAmount}) not equal to authorization value (${decodedXPaymentJson[\"payload\"][\"authorization\"][\"value\"]})`),\n paymentConfigs: [$('Validate & Verify POST Body').first().json.accepts[selectedToken]]\n }\n }\n }\n\n // make sure the fromAddress is the same address the authorization\n if (decodedXPaymentJson[\"payload\"][\"authorization\"][\"from\"].toLowerCase() !== $('Payment Token Configs').first().json.body.fromAddress.toLowerCase()) {\n return {\n error: {\n errorMessage: JSON.stringify(`fromAddress (${$('Payment Token Configs').first().json.body.fromAddress}) not equal to authorization address (${decodedXPaymentJson[\"payload\"][\"authorization\"][\"from\"]})`),\n paymentConfigs: [$('Validate & Verify POST Body').first().json.accepts[selectedToken]]\n }\n }\n }\n\n \n // Add the parsed JSON object to the input\n $input.first().json.decodedXPayment = decodedXPaymentJson;\n\n const splitSig = splitSignature(decodedXPaymentJson.payload.signature);\n $input.first().json.decodedXPayment.payload.r = splitSig.r;\n $input.first().json.decodedXPayment.payload.s = splitSig.s;\n $input.first().json.decodedXPayment.payload.v = splitSig.v;\n\n $input.first().json.contractMethodId = Object.values(paymentTokens).find(\n (entry) => entry.network.toLowerCase() === (decodedXPaymentJson.network || \"\").toLowerCase()\n ).contractMethodId;\n\n $input.first().json.gasLimit = gasLimit;\n\n $input.first().json.paymentConfigs = [($('Validate & Verify POST Body').first().json.accepts)[selectedToken]];\n\n return $input.all();\n} catch (error) {\n // Return an error object if the token format is invalid\n return {\n error: {\n errorMessage: JSON.stringify(estimate), \n paymentConfigs: [($('Validate & Verify POST Body').first().json.accepts)[selectedToken]]\n } \n };\n}\n\nreturn $input.all();\n\n"
},
"typeVersion": 2
},
{
"id": "dad98431-090b-49f3-b35f-81ee8344ecad",
"name": "Payment Token Configs",
"type": "n8n-nodes-base.code",
"onError": "continueRegularOutput",
"position": [
-1632,
896
],
"parameters": {
"jsCode": "// put your X402 resource description here\nconst resourceDescription = \"Swap stablecoins for native tokens cross-chain.\"; \n// the amount of time in seconds the authorization should be good for\nconst timeOut = 60;\n\n// the gas station contract is the same on base, avalanche, and arbitrum\n// Latest version is V2: https://github.com/UXlySoftware/1shot-gas-station\n// This is the \"payTo\" address for x402\nconst gasStationContract = {\n \"8453\": \"0x949EBD97b770bcA5146F24f4F9De815b3F457680\",\n \"43114\": \"0x949EBD97b770bcA5146F24f4F9De815b3F457680\",\n \"42161\": \"0x949EBD97b770bcA5146F24f4F9De815b3F457680\",\n \"59144\": \"0x87435C81ef97D608E426f1f0e2185410C9Ba362A\",\n}\n\n// Got to 1Shot Prompts (https://app.1shotapi.com/1shot-prompts) and import the \n// callDiamondWithEIP3009SignatureToNative function, get its contract method id\n// and put it here\nconst gasStationContractMethodIds = {\n \"base\": \"\",\n \"arbitrum\": \"\",\n \"avalanche\": \"\",\n \"linea\": \"\"\n}\n\n// configure addiotional payment tokens here\nconst paymentTokens = [\n {\n tokenAddress: \"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\",\n maxAmountRequired: 1000,\n chain: \"8453\",\n name: \"USD Coin\",\n version: \"2\",\n network: \"base\",\n contractMethodId: gasStationContractMethodIds[\"base\"]\n },\n {\n tokenAddress: \"0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e\",\n maxAmountRequired: 1000000,\n chain: \"43114\",\n name: \"USD Coin\",\n version: \"2\",\n network: \"avalanche\",\n contractMethodId: gasStationContractMethodIds[\"avalanche\"]\n },\n {\n tokenAddress: \"0xaf88d065e77c8cc2239327c5edb3a432268e5831\",\n maxAmountRequired: 100000,\n chain: \"42161\",\n name: \"USD Coin\",\n version: \"2\",\n network: \"arbitrum\",\n contractMethodId: gasStationContractMethodIds[\"arbitrum\"]\n },\n {\n tokenAddress: \"0xACYOUR_TWILIO_ACCOUNT_SID2435DA\",\n maxAmountRequired: 10000,\n chain: \"59144\",\n name: \"MetaMask USD\",\n version: \"1\",\n network: \"linea\",\n contractMethodId: gasStationContractMethodIds[\"linea\"]\n },\n {\n tokenAddress: \"0x46850aD61C2B7d64d08c9C754F45254596696984\",\n maxAmountRequired: 10000,\n chain: \"42161\",\n name: \"PayPal USD\",\n version: \"1\",\n network: \"arbitrum\",\n contractMethodId: gasStationContractMethodIds[\"arbitrum\"]\n }\n];\n\n$input.first().json.resourceDescription = resourceDescription;\n$input.first().json.timeOut = timeOut;\n$input.first().json.paymentTokens = paymentTokens;\n$input.first().json.gasStationContract = gasStationContract;\n\nreturn $input.all();"
},
"typeVersion": 2
},
{
"id": "dd5f6fd4-f74b-48cd-ab8a-66852214929c",
"name": "Validate & Verify POST Body",
"type": "n8n-nodes-base.code",
"onError": "continueErrorOutput",
"position": [
-1408,
896
],
"parameters": {
"jsCode": "const timeOut = $input.first().json.timeOut;\nconst resourceDescription = $input.first().json.resourceDescription;\nconst paymentTokens = $input.first().json.paymentTokens;\n\n// this helper function will convert our simple payment configs into an x402 \n// error response \nfunction transformConfig(config, payTo, maxAmountRequired, timeout, resource, resourceDescription) {\n return Object.values(config).map(entry => ({\n scheme: \"exact\",\n network: entry.network,\n maxAmountRequired: maxAmountRequired,\n resource: resource, \n description: resourceDescription,\n mimeType: \"\",\n payTo: payTo,\n maxTimeoutSeconds: timeout, // passed in as argument\n asset: entry.tokenAddress,\n extra: {\n name: entry.name,\n version: entry.version\n }\n }));\n}\n\nfunction requireField(name, value) {\n if (value === null || value === undefined || value === \"\") {\n throw new Error(`Missing or empty required field: ${name}`);\n }\n return value;\n}\n\n// this will look up if the requested payment token is one we support\nfunction findPaymentTokenIndex(paymentTokens, fromToken, fromChain) {\n const index = paymentTokens.findIndex(\n (token) =>\n token.tokenAddress.toLowerCase() === (fromToken || \"\").toLowerCase() &&\n token.chain === String(fromChain)\n );\n\n if (index === -1) {\n throw new Error(\n `No matching payment token found for token=${fromToken}, chain=${fromChain}`\n );\n }\n\n return index;\n}\n\nlet fromChain;\nlet fromToken;\nlet fromAmount;\nlet fromAddress;\nlet toChain;\nlet accepts; \n\n// Validate the required input parameters\ntry {\n fromChain = requireField(\"fromChain\", $input.first().json.body.fromChain);\n fromToken = requireField(\"fromToken\", $input.first().json.body.fromToken);\n fromAmount = requireField(\"fromAmount\", $input.first().json.body.fromAmount);\n fromAddress = requireField(\"fromAddress\", $input.first().json.body.fromAddress);\n toChain = requireField(\"toChain\", $input.first().json.body.toChain);\n \n accepts = transformConfig(paymentTokens, $input.first().json.gasStationContract[$('Webhook').first().json.body.fromChain], $('Webhook').first().json.body.fromAmount,timeOut, $input.first().json.webhookUrl, resourceDescription);\n $input.first().json.accepts = accepts;\n} catch (error) {\n console.log(\"asdf: \", accepts)\n return { \n error: {\n errorMessage: \"Missing POST Body Parameters - requires: fromChain, fromToken, fromAmount, fromAddress, tochain\", \n paymentConfigs: (fromAddress && fromAddress !== \"\") ? transformConfig(paymentTokens, fromAddress, $('Webhook').first().json.body.fromAmount,timeOut, $input.first().json.webhookUrl, resourceDescription) : []\n } \n };\n}\n\n// Be ensure the fromToken is one we accept\ntry {\n // find the token & get its contract method id\n const selectedToken = findPaymentTokenIndex(paymentTokens, fromToken, fromChain);\n $input.first().json.selectedToken = selectedToken;\n} catch (error) {\n return { \n error: {\n errorMessage: \"fromToken & fromChain must match one of the supported payment tokens.\", \n paymentConfigs: accepts\n } \n };\n}\n\nreturn $input.all();\n\n"
},
"typeVersion": 2
},
{
"id": "f8f41400-ca0f-410d-a53b-f41d2730c9cc",
"name": "Response: No X-Payment Header2",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-736,
800
],
"parameters": {
"options": {
"responseCode": 402
},
"respondWith": "json",
"responseBody": "={\n \"x402Version\": \"1\",\n \"error\": {{ $('Validate & Verify X-Payment Header').item.json.error.errorMessage }},\n \"accepts\": {{ JSON.stringify($('Validate & Verify X-Payment Header').item.json.error.paymentConfigs) }}\n}"
},
"typeVersion": 1.3
},
{
"id": "2a50dbb1-5078-4b0d-9415-23c418c68b81",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2560,
496
],
"parameters": {
"color": 6,
"width": 616,
"height": 524,
"content": "## x402 Payment Endpoint \n\nThis workflow fragment can be used to monetize any workflow you can build in n8n by accepting stablecoin payments via an API call.\n\nLearn more about the [x402 payment](https://www.x402.org/) protocol. \n\nWatch the [YouTube tutorial](https://youtu.be/SHb5WkggQR4) for a complete walkthrough.\n@[youtube](SHb5WkggQR4)"
},
"typeVersion": 1
},
{
"id": "b05cea5a-9e77-4f85-b98a-2a7c40b2d12b",
"name": "Response: Verify Failed",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-288,
800
],
"parameters": {
"options": {
"responseCode": 402
},
"respondWith": "json",
"responseBody": "={\n \"x402Version\": \"1\",\n \"error\": {{ JSON.stringify($('Simulate Payment').item.json.error.message) }},\n \"accepts\": {{ JSON.stringify($('Validate & Verify POST Body').item.json.accepts) }}\n}"
},
"typeVersion": 1.3
},
{
"id": "f81d3b66-2a3a-4a8f-936f-41da1d86127e",
"name": "Response: Settle Failed",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-64,
608
],
"parameters": {
"options": {
"responseCode": 402
},
"respondWith": "json",
"responseBody": "={\n \"x402Version\": \"1\",\n \"error\": \"Payment Settlement Failed\",\n \"accepts\": {{ JSON.stringify($('Validate & Verify POST Body').item.json.accepts) }}\n}"
},
"typeVersion": 1.3
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "b62f044b-b5cb-4fef-a6c6-445915dbed9a",
"connections": {
"Webhook": {
"main": [
[
{
"node": "Payment Token Configs",
"type": "main",
"index": 0
}
]
]
},
"Simulate Payment": {
"main": [
[
{
"node": "On Successful Payment Simulation",
"type": "main",
"index": 0
}
],
[]
]
},
"Fetch Li.Fi Quote": {
"main": [
[
{
"node": "Validate & Verify X-Payment Header",
"type": "main",
"index": 0
}
],
[
{
"node": "Response: Failed to Get Quote",
"type": "main",
"index": 0
}
]
]
},
"Payment Token Configs": {
"main": [
[
{
"node": "Validate & Verify POST Body",
"type": "main",
"index": 0
}
],
[]
]
},
"1Shot API Submit & Wait": {
"main": [
[
{
"node": "Response: 200 - Payment Successful",
"type": "main",
"index": 0
}
],
[
{
"node": "Response: Settle Failed",
"type": "main",
"index": 0
}
]
]
},
"Validate & Verify POST Body": {
"main": [
[
{
"node": "Fetch Li.Fi Quote",
"type": "main",
"index": 0
}
],
[
{
"node": "Response: Missing or Invalid Request Body Params",
"type": "main",
"index": 0
}
]
]
},
"On Successful Payment Simulation": {
"main": [
[
{
"node": "1Shot API Submit & Wait",
"type": "main",
"index": 0
}
],
[
{
"node": "Response: Verify Failed",
"type": "main",
"index": 0
}
]
]
},
"Validate & Verify X-Payment Header": {
"main": [
[
{
"node": "Simulate Payment",
"type": "main",
"index": 0
}
],
[
{
"node": "Response: No X-Payment Header2",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
oneShotOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
The growing popularity of agentic payments has lead to the development of protocols like x402 where agents and humans can pay for internet resources over standard http protocols using stablecoins.
Source: https://n8n.io/workflows/7206/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.
📡 This workflow serves as the central Alpha Vantage API fetcher for Tesla trading indicators, delivering cleaned 20-point JSON outputs for three timeframes: , , and . It is required by the following a