{
  "name": "Redlynr x402 Paid Workflow",
  "nodes": [
    {
      "parameters": {},
      "id": "node-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [240, 300]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// CUSTOMER CONFIGURATION\n// ============================================================\n// After importing this workflow, paste your Ethereum wallet\n// private key below. n8n stores workflows encrypted in its\n// database — your key is not exposed in the workflow JSON\n// you received. Never share this workflow after filling it in.\n\nconst PRIVATE_KEY = \"0x_your_private_key_here\";\n\nif (!PRIVATE_KEY || PRIVATE_KEY === '0x_your_private_key_here') {\n  throw new Error(\n    'PRIVATE_KEY not set. Open the Customer Configuration node ' +\n    'and paste your Ethereum wallet private key.'\n  );\n}\n\n// ---- Agent identifiers ----\n// CHAIN_ID should be unique per agent task/session so Redlynr tracks\n// each task independently. Change it between separate tasks.\nconst AGENT_ID = \"n8n_agent\";\nconst CHAIN_ID = \"n8n_chain_001\";\n\n// ---- Chain type ----\n// Declare the type of agent chain so Redlynr applies the correct limits.\n// \"interactive\" — conversational agent, human in the loop (default: 20 steps)\n// \"batch\"       — automated pipeline, no human in the loop (default: 100 steps)\n// \"scheduled\"   — recurring scheduled task\n// Always set this explicitly — auto-detection may misclassify n8n workflows.\nconst CHAIN_TYPE = \"interactive\";\n\n// ---- Nesting depth ----\n// 0 = top-level orchestrator, 1 = first-generation sub-agent, etc.\nconst DEPTH = 0;\n\n// ---- Step cost ----\n// Estimated cost of this agent step in USD. Used for cost budget tracking.\nconst STEP_COST = 0.001;\n\n// ---- Retry signal ----\n// Set true if this call is retrying a previously failed step.\nconst IS_RETRY = false;\n\n// ---- Novelty signal ----\n// Set false if this tool call uses identical arguments to a recent call.\n// Set true if the call is genuinely exploring new inputs.\n// Wire your own tool-call tracking logic here if available.\nconst IS_NOVEL_TARGET = true;\n\n// ---- Tool fixation signal ----\n// Integer: how many of the last N calls used the same tool name.\n// 0 = no fixation. Wire your own tracking logic here if available.\nconst TOOL_FIXATION_SIGNAL = 0;\n\n// ============================================================\n\nif (!CHAIN_TYPE) {\n  throw new Error('CHAIN_TYPE not set — set to \"interactive\", \"batch\", or \"scheduled\".');\n}\n\nreturn [{ json: {\n  PRIVATE_KEY,\n  AGENT_ID,\n  CHAIN_ID,\n  CHAIN_TYPE,\n  DEPTH,\n  STEP_COST,\n  IS_RETRY,\n  IS_NOVEL_TARGET,\n  TOOL_FIXATION_SIGNAL\n}}];"
      },
      "id": "node-config",
      "name": "Customer Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [460, 300]
    },
    {
      "parameters": {
        "jsCode": "// Calls Redlynr via x402 paid path.\n// Uses ethers for EIP-712 signing and ethers.FetchRequest for HTTP.\n//\n// Flow:\n//   1. Derive wallet address and tenant_id from private key\n//   2. POST /register (free, unpriced) — ignored if already registered\n//   3. POST /run — receive 402 with payment-required header\n//   4. Parse payment requirements, build EIP-712 TransferWithAuthorization\n//   5. Sign with ethers wallet, build full x402 V2 PaymentPayload\n//   6. Retry POST /run with PAYMENT-SIGNATURE header — receive decision\n\nconst ethers = require('ethers');\n\nconst c = $input.item.json;\n\n// ---------------------------------------------------------------------------\n// Step 1: Derive wallet + tenant_id\n// ---------------------------------------------------------------------------\n\nconst wallet = new ethers.Wallet(c.PRIVATE_KEY);\nconst walletAddress = wallet.address;\nconst tenantId = 'n8n-' + walletAddress.slice(0, 10).toLowerCase();\n\n// ---------------------------------------------------------------------------\n// Step 2: Register tenant (free endpoint, always attempt)\n// ---------------------------------------------------------------------------\n\nconst regReq = new ethers.FetchRequest('https://redlynr.com/register');\nregReq.method = 'POST';\nregReq.body = JSON.stringify({ tenant_id: tenantId });\nregReq.setHeader('Content-Type', 'application/json');\nconst regResp = await regReq.send();\nconst regBody = JSON.parse(regResp.bodyText);\nif (!['registered', 'already_registered'].includes(regBody.status)) {\n  throw new Error('Registration failed: ' + JSON.stringify(regBody));\n}\n\n// ---------------------------------------------------------------------------\n// Step 3: POST /run — expect 402\n// ---------------------------------------------------------------------------\n\nconst runPayload = {\n  tenant_id:            tenantId,\n  agent_id:             c.AGENT_ID,\n  chain_id:             c.CHAIN_ID,\n  chain_type:           c.CHAIN_TYPE,\n  depth:                c.DEPTH,\n  step_cost:            c.STEP_COST,\n  is_retry:             c.IS_RETRY,\n  is_novel_target:      c.IS_NOVEL_TARGET,\n  tool_fixation_signal: c.TOOL_FIXATION_SIGNAL,\n};\n\nconst req1 = new ethers.FetchRequest('https://redlynr.com/run');\nreq1.method = 'POST';\nreq1.body = JSON.stringify(runPayload);\nreq1.setHeader('Content-Type', 'application/json');\nconst resp1 = await req1.send();\n\nif (resp1.statusCode === 200) {\n  return [{ json: JSON.parse(resp1.bodyText) }];\n}\nif (resp1.statusCode !== 402) {\n  throw new Error(`Unexpected status ${resp1.statusCode}: ${resp1.bodyText}`);\n}\n\n// ---------------------------------------------------------------------------\n// Step 4: Parse payment requirements from 402 header\n// ---------------------------------------------------------------------------\n\nconst prHeader = resp1.headers['payment-required'];\nif (!prHeader) {\n  throw new Error('No payment-required header in 402 response');\n}\n\nconst pr     = JSON.parse(Buffer.from(prHeader, 'base64').toString('utf8'));\nconst accept = pr.accepts[0];\n\nconst validAfter  = 0;\nconst validBefore = Math.floor(Date.now() / 1000) + accept.maxTimeoutSeconds;\nconst nonceHex    = ethers.hexlify(ethers.randomBytes(32));\nconst amount      = BigInt(accept.amount);\n\n// ---------------------------------------------------------------------------\n// Step 5: Sign EIP-712 TransferWithAuthorization\n// ---------------------------------------------------------------------------\n\nconst signature = await wallet.signTypedData(\n  {\n    name:              accept.extra.name,\n    version:           accept.extra.version,\n    chainId:           parseInt(accept.network.split(':')[1]),\n    verifyingContract: accept.asset,\n  },\n  {\n    TransferWithAuthorization: [\n      { name: 'from',        type: 'address' },\n      { name: 'to',          type: 'address' },\n      { name: 'value',       type: 'uint256' },\n      { name: 'validAfter',  type: 'uint256' },\n      { name: 'validBefore', type: 'uint256' },\n      { name: 'nonce',       type: 'bytes32' },\n    ],\n  },\n  {\n    from:        walletAddress,\n    to:          accept.payTo,\n    value:       amount,\n    validAfter,\n    validBefore,\n    nonce:       nonceHex,\n  }\n);\n\n// ---------------------------------------------------------------------------\n// Step 6: Build full x402 V2 PaymentPayload and retry\n// ---------------------------------------------------------------------------\n\nconst paymentPayload = {\n  x402Version: 2,\n  payload: {\n    authorization: {\n      from:        walletAddress,\n      to:          accept.payTo,\n      value:       amount.toString(),\n      validAfter:  validAfter.toString(),\n      validBefore: validBefore.toString(),\n      nonce:       nonceHex,\n    },\n    signature,\n  },\n  accepted: {\n    scheme:            accept.scheme,\n    network:           accept.network,\n    asset:             accept.asset,\n    amount:            accept.amount,\n    payTo:             accept.payTo,\n    maxTimeoutSeconds: accept.maxTimeoutSeconds,\n    extra:             accept.extra,\n  },\n  resource: pr.resource,\n};\n\nconst encoded = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');\n\nconst req2 = new ethers.FetchRequest('https://redlynr.com/run');\nreq2.method = 'POST';\nreq2.body = JSON.stringify(runPayload);\nreq2.setHeader('Content-Type', 'application/json');\nreq2.setHeader('PAYMENT-SIGNATURE', encoded);\nreq2.setHeader('Access-Control-Expose-Headers', 'PAYMENT-RESPONSE,X-PAYMENT-RESPONSE');\nconst resp2 = await req2.send();\n\nif (resp2.statusCode !== 200) {\n  throw new Error(`Payment rejected (${resp2.statusCode}): ${resp2.bodyText}`);\n}\n\nreturn [{ json: JSON.parse(resp2.bodyText) }];"
      },
      "id": "node-call-redlynr",
      "name": "Call Redlynr",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [680, 300]
    },
    {
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.decision }}",
                    "rightValue": "proceed",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "output": 0
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.decision }}",
                    "rightValue": "slow_down",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "output": 1
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.decision }}",
                    "rightValue": "stop",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "output": 2
            }
          ]
        }
      },
      "id": "node-route",
      "name": "Route on Decision",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [900, 300]
    },
    {
      "parameters": {
        "jsCode": "// PROCEED — Redlynr confirmed this step is within policy.\n// Wire your agent's next action after this node.\nconst r = $input.item.json;\nconsole.log('Redlynr PROCEED | chain_depth:', r.checks?.chain_depth?.reported_depth,\n  '| steps:', r.checks?.steps_retries?.steps,\n  '| cost_spent:', r.checks?.cost?.spent);\nreturn [{ json: { decision: r.decision, checks: r.checks } }];"
      },
      "id": "node-proceed",
      "name": "PROCEED — Continue Agent",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1120, 100]
    },
    {
      "parameters": {
        "jsCode": "// SLOW_DOWN — Redlynr detected the agent is approaching a limit.\n// The agent can continue but should reduce its rate or prepare to stop.\n// reason will be one of: step_cap_warning, cost_budget_warning, volume_pressure\n// Wire back to Call Redlynr if you want to continue after waiting.\nconst r = $input.item.json;\nconst wait = r.wait_seconds ?? 5;\nconsole.log('Redlynr SLOW_DOWN | reason:', r.reason, '| wait:', wait + 's');\n\nconst start = Date.now();\nwhile (Date.now() - start < wait * 1000) {}\n\nreturn [{ json: { decision: r.decision, reason: r.reason, waited_seconds: wait, checks: r.checks } }];"
      },
      "id": "node-slow-down",
      "name": "SLOW_DOWN — Wait and Retry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1120, 300]
    },
    {
      "parameters": {
        "jsCode": "// STOP — hard limit hit. Agent chain must halt.\n// reason will be one of: agent_paused, retries_exhausted,\n// cost_budget_exceeded, chain_depth_exceeded, step_cap_exceeded,\n// volume_pressure_blocked\nconst r = $input.item.json;\nconsole.log('Redlynr STOP | reason:', r.reason);\nreturn [{ json: { decision: r.decision, reason: r.reason, checks: r.checks } }];"
      },
      "id": "node-stop",
      "name": "STOP — Halt Agent",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1120, 500]
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [[{ "node": "Customer Configuration", "type": "main", "index": 0 }]]
    },
    "Customer Configuration": {
      "main": [[{ "node": "Call Redlynr", "type": "main", "index": 0 }]]
    },
    "Call Redlynr": {
      "main": [[{ "node": "Route on Decision", "type": "main", "index": 0 }]]
    },
    "Route on Decision": {
      "main": [
        [{ "node": "PROCEED — Continue Agent", "type": "main", "index": 0 }],
        [{ "node": "SLOW_DOWN — Wait and Retry", "type": "main", "index": 0 }],
        [{ "node": "STOP — Halt Agent", "type": "main", "index": 0 }]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false,
    "instanceId": "redlynr-workflow-x402-paid-v1"
  }
}
