{
  "id": "DnLNxCSpt2ROet37",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Set dynamic restaurant menu pricing with Anthropic Claude and QR menu APIs",
  "tags": [],
  "nodes": [
    {
      "id": "1d1b157b-647d-4e9c-8bde-03802a5e190a",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        0
      ],
      "parameters": {
        "color": "#FFC800",
        "width": 960,
        "height": 1076,
        "content": "## Demand-Curve Dynamic Menu Pricing\n\nAirline-style surge pricing for restaurant dishes \u2014 prices flex up or down based on real-time POS order velocity, local weather, and nearby events/foot traffic, with an AI brand-damage check before anything goes live on the QR menu.\n\nBuilt for restaurants, QSRs, multi-location operators, and digital menu platforms that want guardrailed dynamic pricing instead of a raw elasticity formula.\n\n### How it works\n1. A cron schedule runs the pricing cycle every 15 minutes during service hours.\n2. Three APIs run in parallel: POS sales velocity, local weather, and nearby events/foot traffic.\n3. A Code node merges them into per-dish demand features: velocity trend, weather multiplier, and event multiplier.\n4. A Code node applies each dish's elasticity coefficient to the demand signal and proposes a new price, capped by a max surge/discount band.\n5. Claude reviews every proposed change for brand-damage risk and approves, caps, or rejects it with reasoning.\n6. A Code node applies Claude's decisions, with a safe no-change fallback if parsing fails.\n7. The approved prices are pushed live to the QR-code digital menu.\n\n### Setup\n1. Import this workflow into n8n.\n2. Add your Anthropic Claude credentials to the Anthropic model node.\n3. Add your POS API credentials to Fetch POS Sales Velocity.\n4. Add your weather API credentials to Fetch Local Weather Conditions.\n5. Add your events/foot-traffic API credentials to Fetch Nearby Events & Foot Traffic.\n6. Add your QR-menu platform API credentials to Push Prices to QR Menu.\n7. Activate the workflow to start the cron cycle.\n\n### Customization\n- Set per-dish elasticity coefficients and surge/discount caps in JS - Run Elasticity Pricing Model.\n- Tighten Claude's brand-damage guardrails (e.g. never surge kids' menu items, cap surge at 15% on holidays).\n- Log every pricing cycle to a database for an audit trail.\n- Add a Slack alert alongside the QR push for real-time visibility."
      },
      "typeVersion": 1
    },
    {
      "id": "610e55d7-1317-447e-8d15-0c5521ff31c8",
      "name": "Sticky Note - Stage 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        64
      ],
      "parameters": {
        "color": 3,
        "width": 1300,
        "height": 944,
        "content": "## Stage 1: Demand Signal Ingestion\n\nA cron trigger fires every 15 minutes. Three APIs run in parallel \u2014 POS order velocity, weather, and nearby events. A Code node merges them into per-dish demand features: velocity trend, weather multiplier, event multiplier."
      },
      "typeVersion": 1
    },
    {
      "id": "f593afa5-daf6-4bb0-970e-e5979ac4a4f9",
      "name": "Sticky Note - Stage 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2528,
        64
      ],
      "parameters": {
        "color": 4,
        "width": 1012,
        "height": 800,
        "content": "## Stage 2: Elasticity Model & Brand-Damage Check\n\nA Code node applies each dish's elasticity coefficient to propose a new price within surge/discount caps. After a short rate-limit wait, Claude reviews each change for brand-damage risk and approves, caps, or rejects it."
      },
      "typeVersion": 1
    },
    {
      "id": "15e2103b-6109-4e1b-a268-a1334ce41577",
      "name": "Sticky Note - Stage 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3664,
        64
      ],
      "parameters": {
        "color": 5,
        "width": 892,
        "height": 800,
        "content": "## Stage 3: Finalize & Push Live\n\nA Code node applies Claude's decisions (with a safe no-change fallback if parsing fails), then the approved prices are pushed live to the restaurant's QR-code digital menu."
      },
      "typeVersion": 1
    },
    {
      "id": "f44c2030-f974-4c8c-ab56-4d4b097c8aac",
      "name": "Cron - Pricing Cycle Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1264,
        528
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d46386fb-5d56-4df5-bebc-401105106fd5",
      "name": "Fetch POS Sales Velocity",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1584,
        272
      ],
      "parameters": {
        "url": "https://api.pos-provider.example.com/v1/locations/current/order-velocity",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "b413db9c-626f-457a-8adb-01f331e5e842",
      "name": "Fetch Local Weather Conditions",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1584,
        528
      ],
      "parameters": {
        "url": "https://api.weather-provider.example.com/v1/current",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d7ccbfca-607f-46b4-85ca-5bbe208898b1",
      "name": "Fetch Nearby Events & Foot Traffic",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1584,
        784
      ],
      "parameters": {
        "url": "https://api.events-provider.example.com/v1/nearby",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3ce176c0-41be-4f8b-979e-2d002af25ed7",
      "name": "Merge - Combine Demand Signals",
      "type": "n8n-nodes-base.merge",
      "position": [
        1920,
        528
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "1bc178ff-94ec-469d-8c08-f88d60ab13b6",
      "name": "JS - Build Demand Signal Baseline",
      "type": "n8n-nodes-base.code",
      "position": [
        2240,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\n\nconst posDishes = item.dishes || item.posDishes || [];\nconst weather = item.weather || {};\nconst events = item.events || item.nearbyEvents || [];\n\n// Weather demand multiplier: cold/rainy conditions boost comfort-food demand, hot/sunny boosts cold-item demand\nconst tempF = weather.temperatureF ?? 70;\nconst condition = (weather.condition || '').toLowerCase();\nconst isRainy = condition.includes('rain') || condition.includes('storm');\nconst isCold = tempF < 50;\nconst isHot = tempF > 85;\n\n// Event foot-traffic multiplier: sum of expected attendance nearby, scaled down to a reasonable multiplier\nconst totalNearbyAttendance = events.reduce((sum, e) => sum + (parseInt(e.expectedAttendance, 10) || 0), 0);\nconst eventFootTrafficMultiplier = 1 + Math.min(0.5, totalNearbyAttendance / 20000);\n\nconst dishFeatures = posDishes.map(d => {\n  const dishName = d.name || d.dishName || 'Unknown Dish';\n  const category = (d.category || '').toLowerCase();\n  const ordersLast15Min = parseInt(d.ordersLast15Min, 10) || 0;\n  const avgOrdersLast15Min = parseFloat(d.avgOrdersLast15Min) || 1;\n  const velocityRatio = avgOrdersLast15Min > 0 ? Math.round((ordersLast15Min / avgOrdersLast15Min) * 100) / 100 : 1;\n\n  let weatherMultiplier = 1;\n  if ((isRainy || isCold) && category.includes('comfort')) weatherMultiplier = 1.15;\n  if (isHot && (category.includes('cold') || category.includes('drink') || category.includes('dessert'))) weatherMultiplier = 1.15;\n\n  return {\n    dishName,\n    category,\n    currentPrice: parseFloat(d.currentPrice) || 0,\n    ordersLast15Min, avgOrdersLast15Min, velocityRatio,\n    weatherMultiplier,\n    eventFootTrafficMultiplier: Math.round(eventFootTrafficMultiplier * 100) / 100,\n    elasticityCoefficient: parseFloat(d.elasticityCoefficient) || 0.5\n  };\n});\n\nreturn {\n  json: {\n    locationId: item.locationId || 'default-location',\n    weatherCondition: weather.condition || 'Unknown',\n    temperatureF: tempF,\n    nearbyEventCount: events.length,\n    totalNearbyAttendance,\n    dishFeatures\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "71427693-2724-4dbd-92f1-1bfa734f42c5",
      "name": "JS - Run Elasticity Pricing Model",
      "type": "n8n-nodes-base.code",
      "position": [
        2560,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst MAX_SURGE_PCT = 20;\nconst MAX_DISCOUNT_PCT = 10;\n\nconst pricingProposals = (item.dishFeatures || []).map(d => {\n  // Combined demand signal: velocity vs baseline, weighted by weather and event multipliers\n  const combinedDemandSignal = d.velocityRatio * d.weatherMultiplier * d.eventFootTrafficMultiplier;\n\n  // Elasticity-adjusted price delta: how much demand deviates from 1.0 (normal), scaled by the dish's elasticity coefficient\n  const demandDeviation = combinedDemandSignal - 1;\n  let pctChange = Math.round(demandDeviation * d.elasticityCoefficient * 100 * 10) / 10;\n\n  // Cap surge and discount bands\n  if (pctChange > MAX_SURGE_PCT) pctChange = MAX_SURGE_PCT;\n  if (pctChange < -MAX_DISCOUNT_PCT) pctChange = -MAX_DISCOUNT_PCT;\n\n  const proposedPrice = Math.round(d.currentPrice * (1 + pctChange / 100) * 100) / 100;\n\n  return {\n    dishName: d.dishName,\n    category: d.category,\n    currentPrice: d.currentPrice,\n    combinedDemandSignal: Math.round(combinedDemandSignal * 100) / 100,\n    proposedPctChange: pctChange,\n    proposedPrice\n  };\n});\n\nreturn {\n  json: {\n    ...item,\n    pricingProposals\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "50fd51d9-e15e-48b9-9e7e-0cd9adc67b65",
      "name": "Wait For Model Buffer",
      "type": "n8n-nodes-base.wait",
      "position": [
        2880,
        528
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "c1f212c4-55c1-4e88-8f6f-ab710f8240f2",
      "name": "Claude - Brand-Damage Check",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3264,
        528
      ],
      "parameters": {
        "text": "=You are a pricing governance reviewer for a restaurant's dynamic menu pricing engine. Weather: {{ $json.weatherCondition }}, {{ $json.temperatureF }}F. Nearby events: {{ $json.nearbyEventCount }} (total expected attendance {{ $json.totalNearbyAttendance }}).\n\nProposed price changes for this cycle:\n{{ JSON.stringify($json.pricingProposals, null, 2) }}\n\nReview each proposed change for brand-damage or customer-trust risk - for example: surging comfort food during bad weather, surging prices near a tragedy/emergency-type event, discounting so often it trains customers to wait, or any single-item surge that looks like price gouging.\n\nFor each dish, decide whether to approve the proposed price as-is, cap it to a smaller change, or reject it (keep current price).\n\nReturn a JSON object with EXACTLY this shape:\n{\n  \"decisions\": [\n    {\n      \"dishName\": \"string, matching the input exactly\",\n      \"decision\": \"One of: Approved, Capped, Rejected\",\n      \"finalPrice\": number,\n      \"reasoning\": \"one short sentence\"\n    }\n  ],\n  \"overallRiskNote\": \"1-2 sentence summary of this cycle's pricing risk\"\n}\n\nReturn ONLY the JSON. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "8fab3581-ee41-4eec-9c49-c6cdee9154a1",
      "name": "Anthropic - Brand Safety Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        3136,
        672
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.2
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "11489e74-71e6-4461-8b00-3feccc406f1b",
      "name": "JS - Parse AI Output & Finalize Approved Prices",
      "type": "n8n-nodes-base.code",
      "position": [
        3776,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch (e) {\n  // Fallback: keep every price unchanged if AI parsing fails\n  parsed = {\n    decisions: (item.pricingProposals || []).map(p => ({\n      dishName: p.dishName,\n      decision: 'Rejected',\n      finalPrice: p.currentPrice,\n      reasoning: 'AI review unavailable this cycle; price held at current value as a safety default.'\n    })),\n    overallRiskNote: 'AI brand-damage check could not be parsed this cycle; all prices held unchanged.'\n  };\n}\n\nconst decisionsByDish = {};\nfor (const d of (parsed.decisions || [])) {\n  decisionsByDish[d.dishName] = d;\n}\n\nconst finalPrices = (item.pricingProposals || []).map(p => {\n  const d = decisionsByDish[p.dishName];\n  return {\n    dishName: p.dishName,\n    currentPrice: p.currentPrice,\n    proposedPrice: p.proposedPrice,\n    decision: d ? d.decision : 'Rejected',\n    finalPrice: d ? d.finalPrice : p.currentPrice,\n    reasoning: d ? d.reasoning : 'No decision returned; price held at current value.'\n  };\n});\n\nreturn {\n  json: {\n    locationId: item.locationId,\n    cycleTimestamp: new Date().toISOString(),\n    overallRiskNote: parsed.overallRiskNote || '',\n    finalPrices\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c26f46cb-563d-401d-8a9e-aff1bf084df7",
      "name": "Push Prices to QR Menu",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4192,
        528
      ],
      "parameters": {
        "url": "https://api.menu-platform.example.com/v1/menus/current/prices",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"locationId\": \"{{ $json.locationId }}\",\n  \"cycleTimestamp\": \"{{ $json.cycleTimestamp }}\",\n  \"prices\": {{ JSON.stringify($json.finalPrices.map(p => ({ dishName: p.dishName, price: p.finalPrice }))) }}\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8b332fd9-1f9d-4ae0-a6eb-8a58c7b2b1a2",
      "name": "Wait For Result",
      "type": "n8n-nodes-base.wait",
      "position": [
        3984,
        528
      ],
      "parameters": {
        "amount": 15
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "0c1d653a-094e-4a02-9a0f-964379f06f22",
  "nodeGroups": [],
  "connections": {
    "Wait For Result": {
      "main": [
        [
          {
            "node": "Push Prices to QR Menu",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait For Model Buffer": {
      "main": [
        [
          {
            "node": "Claude - Brand-Damage Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch POS Sales Velocity": {
      "main": [
        [
          {
            "node": "Merge - Combine Demand Signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude - Brand-Damage Check": {
      "main": [
        [
          {
            "node": "JS - Parse AI Output & Finalize Approved Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cron - Pricing Cycle Trigger": {
      "main": [
        [
          {
            "node": "Fetch POS Sales Velocity",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Local Weather Conditions",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Nearby Events & Foot Traffic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic - Brand Safety Model": {
      "ai_languageModel": [
        [
          {
            "node": "Claude - Brand-Damage Check",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Local Weather Conditions": {
      "main": [
        [
          {
            "node": "Merge - Combine Demand Signals",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge - Combine Demand Signals": {
      "main": [
        [
          {
            "node": "JS - Build Demand Signal Baseline",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Build Demand Signal Baseline": {
      "main": [
        [
          {
            "node": "JS - Run Elasticity Pricing Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Run Elasticity Pricing Model": {
      "main": [
        [
          {
            "node": "Wait For Model Buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse AI Output & Finalize Approved Prices": {
      "main": [
        [
          {
            "node": "Wait For Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}