{
  "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
          }
        ]
      ]
    }
  }
}