AutomationFlowsGeneral › Monetize Workflows with X402 Payment Protocol and 1shot API

Monetize Workflows with X402 Payment Protocol and 1shot API

By1Shot API @oneshotapi on n8n.io

This workflow lets you monetize any n8n workflow with the x402 payment protocol.

Webhook trigger★★★★☆ complexity14 nodesN8N Nodes 1Shot
General Trigger: Webhook Nodes: 14 Complexity: ★★★★☆ Added:
Monetize Workflows with X402 Payment Protocol and 1shot API — n8n workflow card showing N8N Nodes 1Shot integration

This workflow corresponds to n8n.io template #5389 — 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 →

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

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow lets you monetize any n8n workflow with the x402 payment protocol.

Source: https://n8n.io/workflows/5389/ — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

GiveWP Donations to Beacon. Uses httpRequest, stopAndError. Webhook trigger; 43 nodes.

HTTP Request, Stop And Error
General

Use this workflow to book, cancel, or reschedule appointments using Vapi and Google Calendar

Google Calendar
General

Remove Video Background & Compose on Custom Image Background with Google Drive. Uses httpRequest, googleDrive. Webhook trigger; 25 nodes.

HTTP Request, Google Drive
General

AI Website Chatbot — Main Handler. Uses httpRequest. Webhook trigger; 22 nodes.

HTTP Request
General

REST API with Google Sheets. Uses googleSheets, respondToWebhook, stickyNote. Webhook trigger; 17 nodes.

Google Sheets