{
  "id": "EfbdYnnqgNACIN81",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "x402 Gateway Template",
  "tags": [
    {
      "id": "ChlnN7rtKHJMnWmM",
      "name": "x402",
      "createdAt": "2025-06-19T04:23:28.149Z",
      "updatedAt": "2025-06-19T04:23:28.149Z"
    }
  ],
  "nodes": [
    {
      "id": "bf854266-4250-4491-af67-ff8fc3b63ac6",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -768,
        -48
      ],
      "parameters": {
        "path": "92c5ca23-99a7-437d-85da-84aef8bd2a25",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "a2f18d76-8bea-4d1c-9287-6c0a7594d6da",
      "name": "On Successful Payment Simulation",
      "type": "n8n-nodes-base.if",
      "position": [
        128,
        -144
      ],
      "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": "5b29d382-d7dc-484c-b700-3de0c5935c8a",
      "name": "Response: 200 - Payment Successful",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        576,
        -336
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "text",
        "responseBody": "\"Payment Received!\" "
      },
      "typeVersion": 1.3
    },
    {
      "id": "ff4b8de5-6be4-4cce-abc6-05e3902eb535",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1424,
        -400
      ],
      "parameters": {
        "color": 6,
        "width": 536,
        "height": 492,
        "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/m3ThthLtj3g) for a complete walkthrough.\n@[youtube](m3ThthLtj3g)"
      },
      "typeVersion": 1
    },
    {
      "id": "7edff090-85d5-48e6-9d86-203bbf3cea11",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -288
      ],
      "parameters": {
        "width": 584,
        "height": 208,
        "content": "## Configure Payment Tokens\n\n1. Log into 1Shot API and create an API key & Secret, then use those & your business ID to create an n8n credential for the 1Shot API nodes. \n2. Set the `Payment Token Configs` to use the payment token(s) you want to accept, the price and wallet to receive payments in. "
      },
      "typeVersion": 1
    },
    {
      "id": "9b46463f-be85-47e2-a05c-96a696f1965f",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        -512
      ],
      "parameters": {
        "color": 5,
        "width": 496,
        "height": 168,
        "content": "## Put your workflow down here \n\nOnce the payment transaction has been confirmed, replace the `Response: 200 - Payment Successful` block with your workflow which responds to the user with the appropriate premium content. "
      },
      "typeVersion": 1
    },
    {
      "id": "4e74105c-cf34-45bb-a805-9e3dec1b2435",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1072,
        176
      ],
      "parameters": {
        "color": 4,
        "width": 840,
        "height": 280,
        "content": "## Example Curl Command\n\nGenerate x-payment headers with the 1Shot API [x402 tool](https://1shotapi.com/tools). You can test the webhook endpoint with a command like this (be sure to use a properly formatted x-payment header payload): \n\n```sh\n# swap out the URL here for you webhook URL endpoint\ncurl -X GET \\\n  https://n8n.1shotapi.dev/webhook-test/92c5ca23-99a7-437d-85da-84aef8bd2a25 \\\n  -H \"x-payment: YOUR-BASE64-ENCODED-PAYMENT-PAYLOAD\" \\\n  -H \"User-Agent: CustomUserAgent/1.0\" \\\n  -H \"Accept: application/json\"\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "5692fcd3-9b3d-43c2-9144-278be366a557",
      "name": "Response: Missing or Invalid Payment Headers1",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -96,
        48
      ],
      "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": "2ec2cab4-0200-4833-8a1a-7d45ea2d3461",
      "name": "Validate & Verify X-Payment Header",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        -320,
        -48
      ],
      "parameters": {
        "jsCode": "const payTo = $input.first().json.payTo;\nconst timeOut = $input.first().json.timeOut;\nconst resourceDescription = $input.first().json.resourceDescription;\nconst paymentTokens = $input.first().json.paymentTokens;\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  const missing = [];\n\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\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// this helper function will convert our simple payment configs into an x402 \n// error response \nfunction transformConfig(config, payTo, timeout, resource, resourceDescription) {\n  return Object.values(config).map(entry => ({\n    scheme: \"exact\",\n    network: entry.network,\n    maxAmountRequired: entry.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\nconst accepts = transformConfig(paymentTokens, payTo, timeOut, $input.first().json.webhookUrl, resourceDescription);\n$input.first().json.accepts = accepts;\n\n// try to decode the x-payment header if it exists\ntry {\n    // Decode the x-payment header from base64\n    const xPaymentHeader = $input.first().json.headers['x-payment'];\n    const decodedXPayment = Buffer.from(xPaymentHeader, 'base64').toString('utf-8');\n\n    // Parse the decoded value into a JSON object\n    const decodedXPaymentJson = JSON.parse(decodedXPayment);\n\n    const validation = validateXPayment(decodedXPaymentJson)\n    if (validation != \"valid\") {\n      return { \n        error: {\n          errorMessage: validation, \n          paymentConfigs: accepts\n        } \n      };\n    }\n\n    const verification = verifyPaymentDetails(decodedXPaymentJson, paymentTokens, payTo);\n    if (verification != \"valid\") {\n      return {\n        error: {\n          errorMessage: verification,\n          paymentConfigs: accepts\n        }\n      }\n    }\n  \n    // Add the parsed JSON object to the input\n    $input.first().json.decodedXPayment = decodedXPaymentJson;\n    console.log(\"asdf:\", 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    console.log(\"asdf\", $input.first().json.contractMethodId)\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: \"Could not decode x-payment header\", \n        paymentConfigs: accepts\n      } \n    };\n}\n\nreturn $input.all();\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8790cc26-6626-47fe-ae0f-8142b3fde734",
      "name": "Response: Payment Failed",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        576,
        -144
      ],
      "parameters": {
        "options": {
          "responseCode": 402
        },
        "respondWith": "json",
        "responseBody": "={\n  \"x402Version\": \"1\",\n  \"error\": \"X-PAYMENT header did not verify: {{ ($('Settle Payment')?.item.json.failureReason || \"\").replace(/\"/g, \"'\")  }}\",\n  \"accepts\": {{ JSON.stringify($('Validate & Verify X-Payment Header').item.json.accepts) }}\n} "
      },
      "typeVersion": 1.3
    },
    {
      "id": "44804e99-bc8a-4142-b819-665ffc93132e",
      "name": "Response: Payment Invalid",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        352,
        -48
      ],
      "parameters": {
        "options": {
          "responseCode": 402
        },
        "respondWith": "json",
        "responseBody": "={\n  \"x402Version\": \"1\",\n  \"error\": \"X-PAYMENT header did not verify: {{ $('Verify Payment')?.item.json.error?.decodedData?.args?.[0] || \"\" }}\",\n  \"accepts\": {{ JSON.stringify($('Validate & Verify X-Payment Header').item.json.accepts) }}\n} "
      },
      "typeVersion": 1.3
    },
    {
      "id": "12531a0e-a77f-4aab-bce9-b4bc83a6b8c9",
      "name": "Payment Token Configs",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        -544,
        -48
      ],
      "parameters": {
        "jsCode": "// put your resource description here\nconst resourceDescription =  \"x402 Gateway for n8n\"; \n// set the correct address to send payments to\nconst payTo = \"0x9fead8b19c044c2f404dac38b925ea16adaa2954\"; \n// the amount of time in seconds the authorization should be good for\nconst timeOut = 60;\n// use this config to supply the payment token information for the tokens you want to get paid in\n// the contractMethodId comes from the 1Shot API dashboard. Click on the `transferWithAuthorization` method details button and copy its ID from there.\nconst paymentTokens = [\n  {\n    maxAmountRequired: \"1000000\",\n    tokenAddress: \"0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238\",\n    chain: \"11155111\",\n    name: \"USDC\",\n    version: \"2\",\n    contractMethodId: \"\",\n    network: \"sepolia\"\n  },\n  {\n    maxAmountRequired: \"1000000\",\n    tokenAddress: \"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\",\n    chain: \"8453\",\n    name: \"USD Coin\",\n    version: \"2\",\n    contractMethodId: \"\",\n    network: \"base\"\n  },\n  {\n    maxAmountRequired: \"1000000\",\n    tokenAddress: \"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E\",\n    chain: \"43114\",\n    name: \"USD Coin\",\n    version: \"2\",\n    contractMethodId: \"65a87292-2f21-4c35-86ef-b30c64746095\",\n    network: \"avalanche\"\n  },\n  {\n    maxAmountRequired: \"1000000\",\n    tokenAddress: \"0xaf88d065e77c8cc2239327c5edb3a432268e5831\",\n    chain: \"42161\",\n    name: \"USD Coin\",\n    version: \"2\",\n    contractMethodId: \"\",\n    network: \"arbitrum\"\n  }\n];\n\n$input.first().json.resourceDescription = resourceDescription;\n$input.first().json.payTo = payTo;\n$input.first().json.timeOut = timeOut;\n$input.first().json.paymentTokens = paymentTokens;\n\nreturn $input.all();\n\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "1f182441-25da-41cc-913d-8e724866d7d3",
      "name": "Settle Payment",
      "type": "n8n-nodes-1shot.oneShotSynch",
      "onError": "continueRegularOutput",
      "position": [
        352,
        -240
      ],
      "parameters": {
        "params": "={\n\"to\": \"{{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.to }}\",\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}",
        "additionalFields": {
          "memo": "=x402 payment from {{ $('Validate & Verify X-Payment Header').item.json.decodedXPayment.payload.authorization.from }} "
        },
        "contractMethodId": "={{ $('Validate & Verify X-Payment Header').item.json.contractMethodId }}"
      },
      "credentials": {
        "oneShotOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": false
    },
    {
      "id": "6cad76b8-1a56-4028-92f4-a6fb696e1647",
      "name": "Verify Payment",
      "type": "n8n-nodes-1shot.oneShot",
      "onError": "continueRegularOutput",
      "position": [
        -96,
        -144
      ],
      "parameters": {
        "params": "={\n  \"from\": \"{{ $json.decodedXPayment.payload.authorization.from }}\",\n  \"to\": \"{{ $json.decodedXPayment.payload.authorization.to }}\",\n  \"value\": \"{{ $json.decodedXPayment.payload.authorization.value }}\",\n  \"validAfter\": \"{{ $json.decodedXPayment.payload.authorization.validAfter }}\",\n  \"validBefore\": \"{{ $json.decodedXPayment.payload.authorization.validBefore }}\",\n  \"nonce\": \"{{ $json.decodedXPayment.payload.authorization.nonce }}\",\n  \"v\": \"{{ $json.decodedXPayment.payload.v }}\",\n  \"r\": \"{{ $json.decodedXPayment.payload.r }}\",\n  \"s\": \"{{ $json.decodedXPayment.payload.s }}\"\n} ",
        "operation": "simulate",
        "contractMethodId": "={{ $('Validate & Verify X-Payment Header').item.json.contractMethodId }}"
      },
      "credentials": {
        "oneShotOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "executeOnce": false,
      "typeVersion": 1,
      "alwaysOutputData": true
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5b1f688a-5713-4686-a76d-215741de8155",
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Payment Token Configs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settle Payment": {
      "main": [
        [
          {
            "node": "Response: 200 - Payment Successful",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Response: Payment Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Payment": {
      "main": [
        [
          {
            "node": "On Successful Payment Simulation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Payment Token Configs": {
      "main": [
        [
          {
            "node": "Validate & Verify X-Payment Header",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "On Successful Payment Simulation": {
      "main": [
        [
          {
            "node": "Settle Payment",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Response: Payment Invalid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Verify X-Payment Header": {
      "main": [
        [
          {
            "node": "Verify Payment",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Response: Missing or Invalid Payment Headers1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}