{
  "name": "Score Whop clipping campaigns by payout and draft clip briefs with Apify, OpenAI, Google Sheets and Telegram",
  "nodes": [
    {
      "parameters": {
        "content": "## Score today's paid clipping campaigns by what they pay you, then write the brief\n\nContent reward campaigns pay creators per 1,000 views, and the public directory only shows about\nfifty of them at a time. The ones worth posting for change every day: a campaign with a large\nbudget left and few creators pays well on Monday and is picked clean by Friday. Checking that by\nhand means opening fifty pages, doing the same arithmetic fifty times, and reading fifty sets of\nrules to find the one line that gets clips rejected.\n\nThis workflow does that every morning. Apify reads the directory, and n8n does the thinking: a Code\nnode turns each campaign's rate into what one of **your** posts would actually earn at your usual\nview count, and then OpenAI scores the campaign against your niche and the platforms you post on,\npulls the posting requirements out of the campaign's own text, and names the requirement most\nlikely to get a clip rejected. For the campaigns that pass both your pay floor and your fit score,\na second AI step writes the brief: what to make, three opening hooks, the numbered checklist from\nthe campaign's own rules, and the condition under which you should skip it.\n\nEvery campaign scored is written to Google Sheets, including the ones it turned down and why, so\nyou can see a campaign's budget being eaten week by week.\n\n**Nothing here guarantees a payout.** The pay per post is arithmetic on your own view count, not a\npromise, and the rules are read out of the campaign text, so open the campaign page before you post.\n\n### What you need\n- An [Apify account](https://apify.com/?fpr=youssef) and the\n  [Whop Content Rewards Scraper](https://apify.com/fayoussef/whop-clipping-campaigns-scraper?fpr=youssef),\n  which reads the campaign directory, including budget spent, creator counts and the rules text.\n- An OpenAI API key for the two AI steps.\n- Google Sheets, for the record of every campaign scored.\n- Telegram, where the briefs arrive.\n",
        "height": 2000,
        "width": 520,
        "color": 1
      },
      "id": "wc77f201-0000-4000-8000-000000000100",
      "name": "Sticky Note - About",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 1. Your channel, and your pay floor\nEverything downstream is decided here, and two of these fields do most of the work.\n\n`niche`: what your clips are actually about, in plain words. The scorer matches the campaign's\nsubject against this line, so \"poker hands and casino streamer reactions\" scores far better than\n\"entertainment\".\n\n`platforms`: only the platforms you really post on, comma separated, from `tiktok`, `instagram`,\n`youtube` and `x`. A campaign that does not accept one of yours is scored below 30 on purpose,\nbecause there is no way for you to get paid on it.\n\n`typicalViewsPerPost`: your honest median, not your best post. Every pay figure in this workflow is\nyour rate multiplied by this number, so an optimistic value here produces optimistic money\neverywhere.\n\n`wontPost`: the subjects you turn down. Anything naming one of these is scored below 20.\n\n`minPayPerPost` and `minFit` are the two taps: how much one post has to be worth before you want a\nbrief, and how close the subject has to be to your niche.\n",
        "height": 760,
        "width": 520,
        "color": 7
      },
      "id": "wc77f201-0000-4000-8000-000000000101",
      "name": "Sticky Note - Step 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 2. Read the directory, skip what you have already seen\nThe Actor reads the public Content Rewards directory and applies your hard filters before anything\nreaches the AI: minimum dollars per 1,000 views, minimum budget left, how much of the budget is\nalready spent, and how many creators are already submitting.\n\nThose four are the campaign's real economics. A high rate with 94 percent of the budget spent pays\nnobody, and a fresh campaign with 3,000 creators on it is a queue.\n\n**Remove Duplicates** then drops every campaign you were shown on an earlier run, keyed on the\ncampaign id, so a daily run only spends AI tokens on campaigns that are new to you. The Actor also\nhas its own only-new mode, which this template leaves **off**: its first run would return nothing\nat all, and Remove Duplicates already does the same job from the second run onwards.\n",
        "height": 760,
        "width": 580,
        "color": 7
      },
      "id": "wc77f201-0000-4000-8000-000000000102",
      "name": "Sticky Note - Step 2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        360,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 3. The money first, then the fit\n`Work out what it pays you` is a Code node, not an AI step, because arithmetic should not be\nguessed: it turns the campaign's rate into what one of your posts earns at your usual view count,\nworks out the unspent budget per creator already on the campaign, labels the competition low,\nmoderate or crowded, and writes those numbers into one block of plain English.\n\nThe model is then handed that block and told it is not allowed to judge the money, only the fit. It\nscores the campaign against your niche and platforms, lifts the posting requirements out of the\ncampaign's own description, and names the one requirement most likely to get a clip rejected.\n\nIt is told never to invent a watermark rule, a minimum length or a content bank that the campaign\ntext does not mention, and to return \"none published\" when the campaign publishes no rules. That\nhonesty is the point: an invented rule costs you a rejected clip.\n",
        "height": 760,
        "width": 880,
        "color": 7
      },
      "id": "wc77f201-0000-4000-8000-000000000103",
      "name": "Sticky Note - Step 3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        960,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 4. Every campaign, including the ones it turned down\n`Save every campaign` appends one row per campaign per run to the `campaignsTab` tab: the campaign,\nthe brand, the rate, what it pays **you** per post, the budget left, the percentage spent, how many\ncreators are on it, the fit score, the reason, the rules and the date.\n\nThat tab is worth more than any single brief after a few weeks, because you can see a campaign's\nbudget being eaten and spot the ones that are still paying.\n\nOnly campaigns at or above `minFit` that also clear `minPayPerPost` go on to get a brief, capped by\n`maxBriefs`. A campaign whose payout is not published passes the pay floor instead of failing it,\nsince there is no number to test, and the brief tells you to check the page.\n",
        "height": 760,
        "width": 740,
        "color": 7
      },
      "id": "wc77f201-0000-4000-8000-000000000104",
      "name": "Sticky Note - Step 4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1860,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 5. A brief you can start filming from\nThe second AI step writes the thing you actually need: one line on what to make, three opening hooks\nfor the first three seconds, the numbered checklist taken from the campaign's own rules, and one\nline starting \"Skip it if\" naming the condition that makes the campaign not worth your time.\n\nIt is forbidden to promise or estimate earnings beyond the number the Code node worked out, and\nforbidden to add a requirement that is not in the campaign's published rules.\n\nThe brief arrives in Telegram with the campaign link, the fit score, and what one post is worth at\nyour view count. Swap the Telegram node for Discord, Slack or Gmail and nothing else changes.\n",
        "height": 760,
        "width": 880,
        "color": 7
      },
      "id": "wc77f201-0000-4000-8000-000000000105",
      "name": "Sticky Note - Step 5",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2620,
        -240
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "wc77f201-0000-4000-8000-000000000501",
      "name": "Every morning at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -120,
        120
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "wc77f201-0000-4000-8000-000000000200",
              "name": "niche",
              "value": "Poker hands, casino streamer reactions and gambling news, cut from long streams into 30 to 60 second clips with captions",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000201",
              "name": "platforms",
              "value": "tiktok, instagram",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000202",
              "name": "typicalViewsPerPost",
              "value": 20000,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000203",
              "name": "wontPost",
              "value": "crypto trading signals, political content, anything needing my face on camera",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000204",
              "name": "searchText",
              "value": "",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000205",
              "name": "minRewardPerThousand",
              "value": 1,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000206",
              "name": "minBudgetLeftUsd",
              "value": 2000,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000207",
              "name": "maxProgressPercentage",
              "value": 90,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000208",
              "name": "maxCreators",
              "value": 0,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000209",
              "name": "noApplicationRequired",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000210",
              "name": "maxCampaigns",
              "value": 50,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000211",
              "name": "minPayPerPost",
              "value": 15,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000212",
              "name": "minFit",
              "value": 60,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000213",
              "name": "maxBriefs",
              "value": 3,
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000214",
              "name": "googleSheetUrl",
              "value": "PASTE_YOUR_GOOGLE_SHEET_URL_HERE",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000215",
              "name": "campaignsTab",
              "value": "Campaigns",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000216",
              "name": "telegramChatId",
              "value": "PASTE_YOUR_TELEGRAM_CHAT_ID_HERE",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "wc77f201-0000-4000-8000-000000000502",
      "name": "Set your channel",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        120,
        120
      ]
    },
    {
      "parameters": {
        "operation": "Run actor and get dataset",
        "actorSource": "store",
        "actorId": {
          "__rl": true,
          "value": "yAA28P5TTdsL1nOed",
          "mode": "id"
        },
        "customBody": "={{ (() => {\n  const s = $('Set your channel').first().json;\n  const list = v => String(v || '').split(',').map(x => x.trim().toLowerCase()).filter(Boolean);\n  const body = {\n    platforms: list(s.platforms),\n    sortBy: 'budgetLeft',\n    maxItems: Number(s.maxCampaigns) || 50,\n    onlyNewCampaigns: false\n  };\n  if (String(s.searchText || '').trim()) body.searchText = String(s.searchText).trim();\n  if (Number(s.minRewardPerThousand) > 0) body.minRewardPerThousand = Number(s.minRewardPerThousand);\n  if (Number(s.minBudgetLeftUsd) > 0) body.minBudgetLeftUsd = Number(s.minBudgetLeftUsd);\n  if (Number(s.maxProgressPercentage) > 0) body.maxProgressPercentage = Number(s.maxProgressPercentage);\n  if (Number(s.maxCreators) > 0) body.maxCreators = Number(s.maxCreators);\n  if (s.noApplicationRequired === true) body.noApplicationRequired = true;\n  return JSON.stringify(body);\n})() }}"
      },
      "id": "wc77f201-0000-4000-8000-000000000503",
      "name": "Read the campaign directory",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        460,
        120
      ]
    },
    {
      "parameters": {
        "operation": "removeItemsSeenInPreviousExecutions",
        "dedupeValue": "={{ $json.id }}",
        "options": {}
      },
      "id": "wc77f201-0000-4000-8000-000000000504",
      "name": "Skip campaigns already seen",
      "type": "n8n-nodes-base.removeDuplicates",
      "typeVersion": 2,
      "position": [
        720,
        120
      ]
    },
    {
      "parameters": {
        "jsCode": "const s = $('Set your channel').first().json;\nconst views = Number(s.typicalViewsPerPost) || 0;\nconst mine = String(s.platforms || '').toLowerCase().split(',').map(x => x.trim()).filter(Boolean);\nconst fmt = n => Math.round(n).toLocaleString('en-US');\nconst out = [];\n\nfor (const item of $input.all()) {\n  const c = item.json;\n  const rate = c.payoutType === 'cpm' ? Number(c.rewardPerThousandUsd) || 0 : 0;\n  const payPerPost = rate > 0 && views > 0 ? Math.round(rate * views / 10) / 100 : null;\n  const creators = Number(c.creatorsCount) || 0;\n  const budgetLeft = Number(c.budgetLeftUsd) || 0;\n  const spent = Number(c.progressPercentage) || 0;\n  const competition = creators <= 50 ? 'low' : (creators <= 300 ? 'moderate' : 'crowded');\n  const perCreator = creators > 0 ? Math.round(budgetLeft / creators) : null;\n\n  const accepts = (c.socialPlatforms || []).map(p => String(p).toLowerCase());\n  const shared = accepts.filter(p => mine.includes(p));\n  const platformLine = shared.length\n    ? 'Platform overlap: ' + shared.join(', ') + '. The creator posts there, so they can be paid on this campaign.'\n    : 'Platform overlap: none. This campaign accepts ' + (accepts.join(', ') || 'nothing it states')\n      + ', and the creator posts on ' + (mine.join(', ') || 'nothing stated') + '.';\n\n  const lines = [platformLine];\n  if (payPerPost === null) {\n    lines.push('Pays ' + (c.payoutType === 'perPost' ? 'a flat fee per accepted post' : (c.payoutType === 'retainer' ? 'a recurring retainer' : 'in a way the directory does not state')) + ', and the amount is not published, so the pay per post is unknown.');\n  } else {\n    lines.push('Pays ' + rate.toFixed(2) + ' dollars per 1,000 views, which is about ' + payPerPost.toFixed(2) + ' dollars for one post at the creator\\'s usual ' + fmt(views) + ' views.');\n  }\n  lines.push('Budget left ' + fmt(budgetLeft) + ' dollars, with ' + Math.round(spent) + ' percent of the budget already paid out.');\n  lines.push('Creators already submitting: ' + creators + ', which is ' + competition + ' competition'\n    + (perCreator !== null ? ', about ' + fmt(perCreator) + ' dollars of unspent budget per creator' : '') + '.');\n  lines.push(c.requiresApplication ? 'You have to be approved by the brand before your clips count.' : 'No approval step, so clips count from your first post.');\n  if (c.isVerified) lines.push('Whop has verified this brand.');\n\n  out.push({ json: Object.assign({}, c, {\n    expectedPayPerPost: payPerPost,\n    competition,\n    budgetLeftPerCreator: perCreator,\n    platformOverlap: shared.join(', '),\n    hasRules: String(c.description || '').trim().length > 0,\n    economics: lines.join('\\n'),\n  }) });\n}\n\nreturn out;\n"
      },
      "id": "wc77f201-0000-4000-8000-000000000505",
      "name": "Work out what it pays you",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1060,
        120
      ]
    },
    {
      "parameters": {
        "text": "=THE CREATOR\nNiche and what their clips are about: {{ $('Set your channel').first().json.niche }}\nPlatforms they post on: {{ $('Set your channel').first().json.platforms }}\nUsual views per post: {{ $('Set your channel').first().json.typicalViewsPerPost }}\nWill not post: {{ $('Set your channel').first().json.wontPost || 'nothing stated' }}\n\nTHE CAMPAIGN\nTitle: {{ $json.title }}\nBrand: {{ $json.brand || 'not stated' }}\nACCEPTS: {{ ($json.socialPlatforms || []).join(', ') || 'not stated' }}\nLaunched: {{ $json.fundedAgo || 'not stated' }} ago\n\nTHE ECONOMICS, ALREADY WORKED OUT FOR YOU\n{{ $json.economics }}\n\nTHE CAMPAIGN'S OWN RULES, AS PUBLISHED\n{{ $json.description || 'none published' }}",
        "messages": {
          "messageValues": [
            {
              "message": "You are a clipping creator's manager. You decide whether one paid campaign is worth this creator's time, and you read the campaign's own rules so a clip does not get rejected after the work is done. You are strict, because the creator can only post so many clips a day and every wrong pick is a day of unpaid editing.\n\nTwo things are already worked out for you in the text below, and you must not contradict either: the platform overlap and the money. Both are arithmetic, not judgement.\n\nHow to judge fit, 0 to 100. Apply these in order:\n- If the line beginning 'Platform overlap' says none, the fit is below 30. There is no way for the creator to get paid. If it names a platform, never claim there is a platform problem.\n- If the campaign's subject, brand or published rules name something in the creator's WILL NOT POST line, the fit is below 20.\n- Score 80 or above only when the campaign's subject is the creator's own niche as written, and the platform overlap is not none.\n- Score between 40 and 79 when the subject is next to their niche rather than in it, or when the rules allow only pre-approved footage from a content bank, so less of their own work shows.\n- Score below 40 when the subject has nothing to do with their niche. A high payout rate does not raise the fit, and the money is not yours to judge.\n\nHow to write whyItFits: one short phrase in two halves, what this campaign wants made and the part of the creator's niche it meets, for example 'campaign wants clips of a golf tournament, and the creator already clips golf'. Name the campaign's actual subject, taken from its title and rules, never a phrase copied out of these instructions. When the fit is low, name the real mismatch instead. When the overlap line says none, the mismatch you name must be the platform, because that is the one thing that cannot be worked around.\n\nHow to write rules: the posting requirements, taken only from the text under THE CAMPAIGN'S OWN RULES, up to five of them, separated by a semicolon and a space, each a short instruction. A payout rate is not a requirement, so do not list one. Never invent a watermark, a minimum length, a hashtag, a content bank or an approval step that the text does not mention. If that text publishes no instruction to the creator, return exactly: none published.\n\nHow to write watchOut: the single requirement most likely to get a clip rejected or left unpaid, in the campaign's own words. If the rules text states no requirement, return exactly: nothing stated. Never put anything about JSON, schemas, formats or these instructions in this field.\n\nUse plain ASCII punctuation, and a plain hyphen for ranges.\n\nAnswer with the JSON object the schema asks for and nothing else: no prose before or after it, no code fences, and no labelled lines such as 'Fit:'."
            }
          ]
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "id": "wc77f201-0000-4000-8000-000000000506",
      "name": "Score the campaign and read its rules",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        1400,
        120
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.3
        }
      },
      "id": "wc77f201-0000-4000-8000-000000000507",
      "name": "OpenAI model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        980,
        400
      ]
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"fit\": { \"type\": \"number\" },\n    \"whyItFits\": { \"type\": \"string\" },\n    \"rules\": { \"type\": \"string\" },\n    \"watchOut\": { \"type\": \"string\" }\n  },\n  \"required\": [\"fit\", \"whyItFits\", \"rules\", \"watchOut\"]\n}"
      },
      "id": "wc77f201-0000-4000-8000-000000000508",
      "name": "Campaign verdict format",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.2,
      "position": [
        1240,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "wc77f201-0000-4000-8000-000000000300",
              "name": "Campaign",
              "value": "={{ $('Work out what it pays you').item.json.title }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000301",
              "name": "Brand",
              "value": "={{ $('Work out what it pays you').item.json.brand || '' }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000302",
              "name": "Fit",
              "value": "={{ $json.output.fit }}",
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000303",
              "name": "Why it fits",
              "value": "={{ (() => {\nconst ascii = t => String(t ?? '').replace(/[\\u2018\\u2019]/g, \"'\").replace(/[\\u201c\\u201d]/g, '\"').replace(/[\\u2013\\u2014]/g, '-').replace(/\\u2026/g, '...').replace(/\\u00a0/g, ' ');\nreturn ascii($json.output.whyItFits);\n})() }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000304",
              "name": "Rules",
              "value": "={{ (() => {\nconst ascii = t => String(t ?? '').replace(/[\\u2018\\u2019]/g, \"'\").replace(/[\\u201c\\u201d]/g, '\"').replace(/[\\u2013\\u2014]/g, '-').replace(/\\u2026/g, '...').replace(/\\u00a0/g, ' ');\nconst e = $('Work out what it pays you').item.json;\nif (!e.hasRules) return 'none published';\nlet v = ascii($json.output.rules).trim();\nif (!v || /json|schema/i.test(v)) return 'none published';\nconst payOnly = p => /^\\s*(earn|get paid|pays?|payout)\\b/i.test(p) || /\\$\\s?\\d[\\d.,]*\\s*(\\/|per)\\s*1\\s*,?0?0?0?\\s*k?/i.test(p);\nconst parts = v.split(/;\\s*/).map(p => p.trim()).filter(Boolean).filter(p => !payOnly(p));\nif (!parts.length) return 'none published';\nv = parts.join('; ');\nreturn v;\n})() }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000305",
              "name": "Watch out for",
              "value": "={{ (() => {\nconst ascii = t => String(t ?? '').replace(/[\\u2018\\u2019]/g, \"'\").replace(/[\\u201c\\u201d]/g, '\"').replace(/[\\u2013\\u2014]/g, '-').replace(/\\u2026/g, '...').replace(/\\u00a0/g, ' ');\nconst e = $('Work out what it pays you').item.json;\nif (!e.hasRules) return 'nothing stated';\nlet v = ascii($json.output.watchOut).trim();\nif (!v || /json|schema/i.test(v)) return 'nothing stated';\nreturn v;\n})() }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000306",
              "name": "Payout type",
              "value": "={{ $('Work out what it pays you').item.json.payoutType === 'cpm' ? 'Per 1,000 views' : ($('Work out what it pays you').item.json.payoutType === 'perPost' ? 'Flat fee per post' : ($('Work out what it pays you').item.json.payoutType === 'retainer' ? 'Retainer' : 'Not stated')) }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000307",
              "name": "Rate per 1K",
              "value": "={{ $('Work out what it pays you').item.json.rewardDisplay || '' }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000308",
              "name": "Pays you per post",
              "value": "={{ $('Work out what it pays you').item.json.expectedPayPerPost }}",
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000309",
              "name": "Budget left",
              "value": "={{ $('Work out what it pays you').item.json.budgetLeftUsd === null ? '' : '$' + Math.round($('Work out what it pays you').item.json.budgetLeftUsd).toLocaleString('en-US') }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000310",
              "name": "Percent spent",
              "value": "={{ Math.round($('Work out what it pays you').item.json.progressPercentage) }}",
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000311",
              "name": "Creators",
              "value": "={{ $('Work out what it pays you').item.json.creatorsCount }}",
              "type": "number"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000312",
              "name": "Competition",
              "value": "={{ $('Work out what it pays you').item.json.competition }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000313",
              "name": "Platforms",
              "value": "={{ ($('Work out what it pays you').item.json.socialPlatforms || []).join(', ') }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000314",
              "name": "Platform overlap",
              "value": "={{ $('Work out what it pays you').item.json.platformOverlap || 'none' }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000315",
              "name": "Needs approval",
              "value": "={{ $('Work out what it pays you').item.json.requiresApplication ? 'Yes' : 'No' }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000316",
              "name": "Launched",
              "value": "={{ $('Work out what it pays you').item.json.fundedAgo || '' }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000317",
              "name": "Campaign link",
              "value": "={{ $('Work out what it pays you').item.json.campaignUrl || '' }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000318",
              "name": "Economics",
              "value": "={{ $('Work out what it pays you').item.json.economics }}",
              "type": "string"
            },
            {
              "id": "wc77f201-0000-4000-8000-000000000319",
              "name": "Checked",
              "value": "={{ $now.format('yyyy-MM-dd') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "wc77f201-0000-4000-8000-000000000509",
      "name": "Format the campaign row",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1960,
        120
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Set your channel').first().json.googleSheetUrl }}",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Set your channel').first().json.campaignsTab }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "wc77f201-0000-4000-8000-000000000510",
      "name": "Save every campaign",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2300,
        20
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "wc77f201-0000-4000-8000-000000000400",
              "leftValue": "={{ (() => {\nconst s = $('Set your channel').first().json;\nconst pay = $json['Pays you per post'];\nconst payOk = pay === null || pay === undefined || pay === '' ? true : Number(pay) >= Number(s.minPayPerPost);\nreturn Number($json.Fit) >= Number(s.minFit) && payOk;\n})() }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "wc77f201-0000-4000-8000-000000000511",
      "name": "Keep the campaigns worth clipping",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        2300,
        240
      ]
    },
    {
      "parameters": {
        "maxItems": "={{ $('Set your channel').first().json.maxBriefs }}"
      },
      "id": "wc77f201-0000-4000-8000-000000000512",
      "name": "Cap the briefs per run",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        2720,
        240
      ]
    },
    {
      "parameters": {
        "text": "=THE CREATOR\nNiche and what their clips are about: {{ $('Set your channel').first().json.niche }}\nPlatforms they post on: {{ $('Set your channel').first().json.platforms }}\n\nTHE CAMPAIGN\nTitle: {{ $json.Campaign }}\nBrand: {{ $json.Brand || 'not stated' }}\nAccepts: {{ $json.Platforms }}\nWhy it fits this creator: {{ $json['Why it fits'] }}\n\nTHE ECONOMICS\n{{ $json.Economics }}\n\nTHE RULES\n{{ $json.Rules }}\n\nTHE THING THAT GETS A CLIP REJECTED\n{{ $json['Watch out for'] }}",
        "messages": {
          "messageValues": [
            {
              "message": "You write the short brief a clipping creator reads before they start editing. They have already decided to work this campaign. Your job is to tell them what to make and what will get the clip rejected, in the fewest words that are still usable.\n\nWrite exactly this, with nothing before it and nothing after it:\n\nOne line saying what to make: the kind of clip, the subject, and the platform it is for. Build it from the campaign's subject and the creator's own niche as written above, and use only a platform named under Accepts.\n\nThen exactly three lines, each starting with a plain hyphen and a space: three different opening hooks for the first three seconds, at most 12 words each. A hook is what the viewer hears while they look at the footage, so it is about what is happening on screen. A hook is never about money, earning, clipping, this campaign or being paid: 'did you know you can earn from your clips' is addressed to the wrong person and is wrong. No hashtags, no emoji, no Title Case.\n\nThen a line that reads exactly: Before you post:\nThen two to five numbered lines ('1. ', '2. ' and so on), each one requirement taken only from the text under THE RULES. Copy the requirement, shortened if needed. A payout rate is not a requirement and neither is the absence of an approval step. If THE RULES says 'none published', or holds nothing but payment terms, write one numbered line saying to open the campaign page and read the requirements before posting, and nothing else.\n\nThen one last line starting with 'Skip it if ' naming the one condition that would make this campaign not worth the creator's time, and quote the number it rests on from THE ECONOMICS, for example 'Skip it if you cannot post this week, since only 4,702 dollars of the budget is left' or 'Skip it if you are not approved, since clips before approval do not count'.\n\nForbidden anywhere: a promise or an estimate of earnings beyond the number already given above, 'guaranteed', 'viral', 'easy money', 'blow up', 'insane', 'crush it', exclamation marks, emoji, markdown headings, bold, any requirement that is not in THE RULES, and any claim about this brand beyond what is written above. Do not put a space at the end of a line. Use plain ASCII punctuation and a plain hyphen for ranges. Never mention that this was written by a model or an automation."
            }
          ]
        },
        "promptType": "define"
      },
      "id": "wc77f201-0000-4000-8000-000000000513",
      "name": "Write the clip brief",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        2980,
        240
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $('Set your channel').first().json.telegramChatId }}",
        "text": "={{ (() => {\nconst esc = s => String(s ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\nconst cut = (s, n) => String(s ?? '').length > n ? String(s).slice(0, n - 1).trimEnd() + '\\u2026' : String(s ?? '');\nconst r = $('Format the campaign row').item.json;\nconst ascii = t => String(t ?? '').replace(/[\\u2018\\u2019]/g, \"'\").replace(/[\\u201c\\u201d]/g, '\"').replace(/[\\u2013\\u2014]/g, '-').replace(/\\u2026/g, '...').replace(/\\u00a0/g, ' ');\nconst tidy = t => ascii(t).split('\\n').map(l => l.replace(/[ \\t]+$/, '')).join('\\n').replace(/\\n{3,}/g, '\\n\\n').trim();\nconst plan = tidy(String($json.text ?? '').replace(/!+/g, '.').replace(/\\n(Before you post:)/g, '\\n\\n$1').replace(/\\n(Skip it if)/gi, '\\n\\n$1'));\nconst pay = r['Pays you per post'];\nreturn '\\ud83c\\udfac <b>' + esc(r['Campaign']) + '</b> \\u00b7 fit ' + r['Fit'] + '/100\\n'\n  + '\\ud83c\\udff7 ' + esc(r['Brand'] || 'brand not stated') + ' \\u00b7 ' + esc(r['Platforms']) + '\\n'\n  + '\\ud83d\\udcb5 ' + (pay === null || pay === undefined || pay === '' ? 'payout not published' : pay + ' dollars a post at your usual views') + ' \\u00b7 ' + esc(r['Rate per 1K']) + ' per 1K\\n'\n  + '\\ud83d\\udcb0 ' + esc(r['Budget left']) + ' left \\u00b7 ' + r['Percent spent'] + '% spent \\u00b7 ' + r['Creators'] + ' creators (' + esc(r['Competition']) + ')\\n'\n  + '\\ud83e\\udde9 ' + esc(r['Why it fits']) + '\\n\\n'\n  + '<b>Your brief</b>\\n<pre>' + esc(cut(plan, 2600)) + '</pre>\\n\\n'\n  + (r['Watch out for'] === 'nothing stated' ? '' : '\\u26a0 ' + esc(cut(r['Watch out for'], 220)) + '\\n')\n  + '\\u27a1 <a href=\"' + r['Campaign link'] + '\">Open the campaign</a>';\n})() }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "id": "wc77f201-0000-4000-8000-000000000514",
      "name": "Send the brief to Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        3240,
        240
      ]
    }
  ],
  "connections": {
    "Every morning at 8am": {
      "main": [
        [
          {
            "node": "Set your channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set your channel": {
      "main": [
        [
          {
            "node": "Read the campaign directory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the campaign directory": {
      "main": [
        [
          {
            "node": "Skip campaigns already seen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip campaigns already seen": {
      "main": [
        [
          {
            "node": "Work out what it pays you",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Work out what it pays you": {
      "main": [
        [
          {
            "node": "Score the campaign and read its rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score the campaign and read its rules": {
      "main": [
        [
          {
            "node": "Format the campaign row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format the campaign row": {
      "main": [
        [
          {
            "node": "Save every campaign",
            "type": "main",
            "index": 0
          },
          {
            "node": "Keep the campaigns worth clipping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep the campaigns worth clipping": {
      "main": [
        [
          {
            "node": "Cap the briefs per run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cap the briefs per run": {
      "main": [
        [
          {
            "node": "Write the clip brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the clip brief": {
      "main": [
        [
          {
            "node": "Send the brief to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI model": {
      "ai_languageModel": [
        [
          {
            "node": "Score the campaign and read its rules",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Write the clip brief",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Campaign verdict format": {
      "ai_outputParser": [
        [
          {
            "node": "Score the campaign and read its rules",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}