{
  "name": "Congress Tracker",
  "nodes": [
    {
      "parameters": {
        "url": "=https://disclosures-clerk.house.gov/public_disc/financial-pdfs/{{ $json.year }}FD.zip",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1504,
        1200
      ],
      "id": "2de44926-675d-462f-9739-3783689fded4",
      "name": "Download FD Index ZIP",
      "executeOnce": false
    },
    {
      "parameters": {},
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [
        2784,
        2144
      ],
      "id": "1c960f5e-10a7-4d67-961b-397876e63136",
      "name": "Calculator1"
    },
    {
      "parameters": {},
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [
        2768,
        2512
      ],
      "id": "d98cb712-2dc8-446e-9869-f5ed85bc40bc",
      "name": "Calculator2"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.compression",
      "typeVersion": 1.1,
      "position": [
        1696,
        1200
      ],
      "id": "d2eb96b2-1685-4155-8958-ccd5c5ba70e6",
      "name": "Unzip FD Index1"
    },
    {
      "parameters": {
        "operation": "xml",
        "binaryPropertyName": "=file_1",
        "destinationKey": "results",
        "options": {
          "encoding": "utf8"
        }
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        1888,
        1200
      ],
      "id": "484246f8-156e-46f9-a176-0b1efd0d4893",
      "name": "Read XML Index1"
    },
    {
      "parameters": {
        "content": "## Get Most Recent Documents",
        "height": 1456,
        "width": 3476,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        304
      ],
      "typeVersion": 1,
      "id": "49239010-06c8-40e7-9c0a-176efa47588c",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "## 🔍 Find & Download  PDF",
        "height": 2032,
        "width": 2000,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        1776
      ],
      "typeVersion": 1,
      "id": "375ff7ab-3c0c-4a59-a160-bce24dfdb888",
      "name": "Sticky Note7"
    },
    {
      "parameters": {
        "content": "## 🤖 PTR AI Extraction & Positions Calculation\n",
        "height": 928,
        "width": 784,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2480,
        1776
      ],
      "typeVersion": 1,
      "id": "a2ba3135-9c0c-464a-a115-a2b8b382f8b9",
      "name": "Sticky Note8"
    },
    {
      "parameters": {
        "url": "=https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/{{ $('Congress Trigger').item.json.year }}/{{ $('Congress Trigger').item.json.doc_id }}.pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1696,
        2400
      ],
      "id": "a25f305e-4ca4-4681-9808-cbb75523ecc6",
      "name": "Download PTR PDF"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "a324f477-6df1-4f45-9a4a-7877b55841d9",
                    "leftValue": "={{ $('Congress Trigger').item.json.report_type }}",
                    "rightValue": "P",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "PTR Document"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "3b5b8ec4-7402-47a9-85c6-5544ed0c8805",
                    "leftValue": "={{ $('Congress Trigger').item.json.report_type }}",
                    "rightValue": "O",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "FD Document"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $('Congress Trigger').item.json.doc_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "notExists",
                      "singleValue": true
                    },
                    "id": "200fe828-e7cb-4699-bdd7-70b457d06c6b"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "No Doc Found"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        1328,
        2576
      ],
      "id": "50e349dc-cb7a-46c3-a237-863c78cfa1ed",
      "name": "Switch"
    },
    {
      "parameters": {
        "url": "=https://disclosures-clerk.house.gov/public_disc/financial-pdfs/{{ $('Congress Trigger').item.json.year }}/{{ $('Congress Trigger').item.json.doc_id }}.pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1936,
        2592
      ],
      "id": "848ed416-7bc9-484d-8651-fdfb36ff0de5",
      "name": "Download FD PDF"
    },
    {
      "parameters": {
        "inputSource": "jsonExample",
        "jsonExample": "{\n    \"congress_member_id\": \"HEJFhM_gyoj-LxUEKhO32\",\n    \"strategy_id\": \"gAF3OE_Wg70JvtZppwJKe\",\n    \"multiplier\": \"240879000.000000000000000\",\n    \"first\": \"Nancy\",\n    \"last\": \"Pelosi\",\n    \"doc_id\": \"10066169\",\n    \"report_type\": \"O\",\n    \"filed_at\": \"5/15/2025\",\n    \"year\": 2024\n  }"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        640,
        2592
      ],
      "id": "b29de3c9-41ce-4091-af45-8a2774043ed7",
      "name": "Congress Trigger"
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        2112,
        2592
      ],
      "id": "53840721-e8ea-4a50-abc6-134f3fd0820b",
      "name": "Extract Text from FD PDF"
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        1888,
        2400
      ],
      "id": "344ed795-f087-43a2-becb-a3e1f203459f",
      "name": "Extract Text from PTR PDF"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "c0571a4a-cf9f-4635-ac7c-0c7c2cba19a5",
              "leftValue": "={{ $('Extract Text from FD PDF').item.json.text }}",
              "rightValue": "=",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2272,
        2592
      ],
      "id": "15e14ba0-38ba-4603-a267-c004a3dd188c",
      "name": "Has FD Text?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "c0571a4a-cf9f-4635-ac7c-0c7c2cba19a5",
              "leftValue": "={{ $('Extract Text from PTR PDF').item.json.text }}",
              "rightValue": "=",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2176,
        2400
      ],
      "id": "16bf24eb-071e-45bf-b0f5-7d59c2a23b86",
      "name": "Has PTR Text?"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=# Input\n- DocID: {{ $('Congress Trigger').item.json.doc_id }}\n- FilingDate: {{ $('Congress Trigger').item.json.filed_at }}\n- FilingType: {{ $('Congress Trigger').item.json.report_type }}\n- Year: {{ $('Congress Trigger').item.json.year }}\n\n# Data\n{{ $json.text }}",
        "options": {
          "systemMessage": "You are a financial data extractor. Extract all stock and investment transactions from a congressional Periodic Transaction Report (PTR).\n\n## Input:\nThe user will provide input in this format:\n\n### Inputs\n- **DocID**: (String) The ID of the PTR document\n- **FilingDate**: (String) The date of the PTR document filing\n- **FilingType**: (String) Represents PTR document type\n- **Year**: (String) Year of the filing date\n\n### Data\nA congressional Periodic Transaction Report (PTR) PDF filed with the Clerk of the House of Representatives. The report contains:\n- **Filer Information**: Name, Status, State/District\n- **Filing Information**: Filing Type, Filing Year, Filing Date\n- **Transactions**: A table with columns: ID, Owner, Asset, Transaction Type, Date, Notification Date, Amount, Cap. Gains > $200?. The **Transaction Type** column uses abbreviations: `P` (Purchase), `S` (full sale — **closes out** the entire position in that asset), `S (partial)` (partial sale — only **part** of the position is sold), `E` (Exchange). Treat **`S`** as a **full exit** and **`S (partial)`** as a **partial exit** unless the Description clearly overrides. Each asset name includes a type code in brackets (e.g. [ST], [OP], [EF], [MF]) and may include a ticker symbol in parentheses (e.g. (AAPL), (MSFT)). Some transactions have additional Description or Comment lines beneath them.\n\n## Rules:\n- Extract ONLY transaction entries from the PTR\n- Include ONLY transactions that have a ticker symbol in parentheses, e.g. (AAPL), (MSFT), (GOOGL)\n- Extract only these asset type codes: [ST] (stocks), [EF] (ETFs), [MF] (mutual funds)\n- Ignore all other asset type codes: [RP] (real property), [BA] (bank accounts), [OL] (partnerships/LLCs), [AB] (other assets), [PS] (private stock), [OP] (options)\n- Skip any transaction where Amount is \"None\" or missing\n- Skip transactions where Amount is a small fixed dollar value (e.g. `$15.00`, `$1.00`) — these are trivial spinoff cash-in-lieu or expired options, not meaningful trades\n- For mutual funds without a ticker symbol in parentheses, skip them\n\n### Action Determination\nDetermine **`Action`** for **each extracted row** **before** aggregation. Use the **Transaction Type** column as the **primary** signal; use **Description** only when the type is ambiguous.\n\n| Transaction Type | Typical **`Action`** |\n|------------------|----------------------|\n| `P` | `buy` |\n| `S (partial)` | `sell` (partial — filer still holds a remainder) |\n| `S` | `close` (full sale — **entire** position closed out) |\n| `E` | `buy` if shares are **received**; otherwise infer from description |\n\nGuidelines when the type alone is unclear:\n- **`S (partial)`** → **`sell`**\n- **`S`** without “partial” → **`close`** (full exit), unless the Description clearly shows a partial sale mislabeled\n- A purchase (`P`) or an exchange that results in acquiring shares → **`buy`**\n\nUse your best judgment when the PDF conflicts with these defaults.\n\n### Aggregating to one row per Symbol + AssetType\nPTR tables often repeat the same ticker on multiple lines. **After** extracting all qualifying rows and assigning each row an **`Action`** (`buy`, `sell`, or `close`), **collapse** to **exactly one** output object per **`Symbol`** + **`AssetType`**.\n\n**Parse** every row’s **Amount** into a lower bound **L** and upper bound **U** (e.g. `$1,001 - $15,000` → L = 1001, U = 15000). Handle commas. If a row uses a single-value form, set **L** and **U** to that value.\n\n#### When the group includes a `close` row (`S` — full exit)\nA **`close`** means the filer **liquidated the entire position** in that **Symbol** + **AssetType**. **Do not** dollar-net **`close`** against **`buy`** the same way as partial **`sell`** rows — the **outcome** is **full exit**, not a residual net long/short from subtracting ranges.\n\n1. **Group** by **`Symbol`** (normalized) and **`AssetType`**.\n\n2. If **any** row has **`Action`** = **`close`**:\n   - Output **one** object with **`Action`** = **`close`**.\n   - Set **`ValueRange`** and **`Midpoint`** from the **`close`** row(s) only: if there is **one** **`close`** row, use its **Amount**; if there are **multiple** **`close`** rows for this symbol, **sum** their **L** and **U** into a combined range and set **`Midpoint`** = **round((Σ L_close + Σ U_close) / 2)**.\n   - **`buy`** and **`sell`** rows in the same group inform context only; they **do not** change **`ValueRange`** or **`Midpoint`** for the **`close`** output (the **`close`** reflects **exiting the whole position**).\n\n3. **`TransactionDate`**: Prefer the **`close`** row’s date if there is a single **`close`**; if multiple **`close`** rows with different dates, use the **earliest** **`close`** date.\n\n#### When the group has no `close` row (only `buy` and/or `sell`)\n**Net** partial sells against acquisitions — **`sell`** (`S (partial)`) amounts **subtract** from **`buy`** amounts. **`close`** is not in this branch.\n\n1. **Σ L (buys)** / **Σ U (buys)** = sums over rows with **`Action`** = `buy`\n2. **Σ L (sells)** / **Σ U (sells)** = sums over rows with **`Action`** = `sell` **only**\n3. **Net_L** = Σ L (buys) − Σ L (sells); **Net_U** = Σ U (buys) − Σ U (sells)\n4. **Midpoint** = **round((Net_L + Net_U) / 2)** (equals sum of buy midpoints minus sum of **partial** sell midpoints)\n\n**Output** — one object per group:\n- If **Midpoint** is **0** or netting cancels out to no meaningful position, **omit** this **Symbol** + **AssetType**.\n- If **Midpoint** **>** **0**: **`Action`** = **`buy`**, **`ValueRange`** from **Net_L** / **Net_U** (same style as the PDF), or bracket-style from **Midpoint** if needed.\n- If **Midpoint** **<** **0**: **`Action`** = **`sell`**, **`Midpoint`** = **|Midpoint|**, **`ValueRange`** from absolute net bounds (ascending order) or from **Midpoint**.\n\n**`TransactionDate`**: **Earliest** date in the group when dates differ.\n\n**Metadata** (`DocID`, `FilingDate`, `FilingType`, `Year`): pass through from **Inputs** unchanged on every output object.\n\n## Final Output Instructions:\n- Output **ONLY** valid JSON. No explanations, no markdown, no extra text.\n- Output a JSON array of objects. Each item must follow this exact structure:\n  `{\n    \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n    \"AssetType\": \"(String) ST for stock, EF for ETF, MF for mutual fund\",\n    \"Action\": \"(String) buy, sell, or close\",\n    \"ValueRange\": \"(String) disclosure dollar range from the PDF, or combined after aggregation — for Action close, from the full-exit S row(s); for buy or sell, from net buys minus partial sells when the group has no close\",\n    \"Midpoint\": \"(Number) whole-dollar midpoint of ValueRange — for Action close, from the close row(s); for buy or sell, (Net_L + Net_U) / 2 after netting, or absolute value when Action is sell\",\n    \"TransactionDate\": \"(String) date as written e.g. 01/15/2024\",\n    \"DocID\": \"(String) pass through from Inputs\",\n    \"FilingDate\": \"(String) pass through from Inputs\",\n    \"FilingType\": \"(String) pass through from Inputs\",\n    \"Year\": \"(String) pass through from Inputs\"\n  }`"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2640,
        1952
      ],
      "id": "61951749-d541-45e9-b52d-3ce7e3fa17c9",
      "name": "Extract from PTR",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5.4",
          "mode": "list",
          "cachedResultName": "GPT-5.4"
        },
        "responses": {
          "values": [
            {
              "type": "file",
              "fileType": "base64",
              "binaryPropertyName": "={{ $('Download PTR PDF').item.binary.data }}",
              "fileName": "Data"
            },
            {
              "content": "=# Input\n- DocID: {{ $('Congress Trigger').item.json.doc_id }}\n- FilingDate: {{ $('Congress Trigger').item.json.filed_at }}\n- FilingType: {{ $('Congress Trigger').item.json.report_type }}\n- Year: {{ $('Congress Trigger').item.json.year }}\n"
            },
            {
              "role": "system",
              "content": "=You are a financial data extractor. Extract all stock and investment transactions from a congressional Periodic Transaction Report (PTR).\n\n## Input:\nThe user will provide input in this format:\n\n### Inputs\n- **DocID**: (String) The ID of the PTR document\n- **FilingDate**: (String) The date of the PTR document filing\n- **FilingType**: (String) Represents PTR document type\n- **Year**: (String) Year of the filing date\n\n### Data\nA congressional Periodic Transaction Report (PTR) PDF filed with the Clerk of the House of Representatives. The report contains:\n- **Filer Information**: Name, Status, State/District\n- **Filing Information**: Filing Type, Filing Year, Filing Date\n- **Transactions**: A table with columns: ID, Owner, Asset, Transaction Type, Date, Notification Date, Amount, Cap. Gains > $200?. The **Transaction Type** column uses abbreviations: `P` (Purchase), `S` (full sale — **closes out** the entire position in that asset), `S (partial)` (partial sale — only **part** of the position is sold), `E` (Exchange). Treat **`S`** as a **full exit** and **`S (partial)`** as a **partial exit** unless the Description clearly overrides. Each asset name includes a type code in brackets (e.g. [ST], [OP], [EF], [MF]) and may include a ticker symbol in parentheses (e.g. (AAPL), (MSFT)). Some transactions have additional Description or Comment lines beneath them.\n\n## Rules:\n- Extract ONLY transaction entries from the PTR\n- Include ONLY transactions that have a ticker symbol in parentheses, e.g. (AAPL), (MSFT), (GOOGL)\n- Extract only these asset type codes: [ST] (stocks), [EF] (ETFs), [MF] (mutual funds)\n- Ignore all other asset type codes: [RP] (real property), [BA] (bank accounts), [OL] (partnerships/LLCs), [AB] (other assets), [PS] (private stock), [OP] (options)\n- Skip any transaction where Amount is \"None\" or missing\n- Skip transactions where Amount is a small fixed dollar value (e.g. `$15.00`, `$1.00`) — these are trivial spinoff cash-in-lieu or expired options, not meaningful trades\n- For mutual funds without a ticker symbol in parentheses, skip them\n\n### Action Determination\nDetermine **`Action`** for **each extracted row** **before** aggregation. Use the **Transaction Type** column as the **primary** signal; use **Description** only when the type is ambiguous.\n\n| Transaction Type | Typical **`Action`** |\n|------------------|----------------------|\n| `P` | `buy` |\n| `S (partial)` | `sell` (partial — filer still holds a remainder) |\n| `S` | `close` (full sale — **entire** position closed out) |\n| `E` | `buy` if shares are **received**; otherwise infer from description |\n\nGuidelines when the type alone is unclear:\n- **`S (partial)`** → **`sell`**\n- **`S`** without “partial” → **`close`** (full exit), unless the Description clearly shows a partial sale mislabeled\n- A purchase (`P`) or an exchange that results in acquiring shares → **`buy`**\n\nUse your best judgment when the PDF conflicts with these defaults.\n\n### Aggregating to one row per Symbol + AssetType\nPTR tables often repeat the same ticker on multiple lines. **After** extracting all qualifying rows and assigning each row an **`Action`** (`buy`, `sell`, or `close`), **collapse** to **exactly one** output object per **`Symbol`** + **`AssetType`**.\n\n**Parse** every row’s **Amount** into a lower bound **L** and upper bound **U** (e.g. `$1,001 - $15,000` → L = 1001, U = 15000). Handle commas. If a row uses a single-value form, set **L** and **U** to that value.\n\n#### When the group includes a `close` row (`S` — full exit)\nA **`close`** means the filer **liquidated the entire position** in that **Symbol** + **AssetType**. **Do not** dollar-net **`close`** against **`buy`** the same way as partial **`sell`** rows — the **outcome** is **full exit**, not a residual net long/short from subtracting ranges.\n\n1. **Group** by **`Symbol`** (normalized) and **`AssetType`**.\n\n2. If **any** row has **`Action`** = **`close`**:\n   - Output **one** object with **`Action`** = **`close`**.\n   - Set **`ValueRange`** and **`Midpoint`** from the **`close`** row(s) only: if there is **one** **`close`** row, use its **Amount**; if there are **multiple** **`close`** rows for this symbol, **sum** their **L** and **U** into a combined range and set **`Midpoint`** = **round((Σ L_close + Σ U_close) / 2)**.\n   - **`buy`** and **`sell`** rows in the same group inform context only; they **do not** change **`ValueRange`** or **`Midpoint`** for the **`close`** output (the **`close`** reflects **exiting the whole position**).\n\n3. **`TransactionDate`**: Prefer the **`close`** row’s date if there is a single **`close`**; if multiple **`close`** rows with different dates, use the **earliest** **`close`** date.\n\n#### When the group has no `close` row (only `buy` and/or `sell`)\n**Net** partial sells against acquisitions — **`sell`** (`S (partial)`) amounts **subtract** from **`buy`** amounts. **`close`** is not in this branch.\n\n1. **Σ L (buys)** / **Σ U (buys)** = sums over rows with **`Action`** = `buy`\n2. **Σ L (sells)** / **Σ U (sells)** = sums over rows with **`Action`** = `sell` **only**\n3. **Net_L** = Σ L (buys) − Σ L (sells); **Net_U** = Σ U (buys) − Σ U (sells)\n4. **Midpoint** = **round((Net_L + Net_U) / 2)** (equals sum of buy midpoints minus sum of **partial** sell midpoints)\n\n**Output** — one object per group:\n- If **Midpoint** is **0** or netting cancels out to no meaningful position, **omit** this **Symbol** + **AssetType**.\n- If **Midpoint** **>** **0**: **`Action`** = **`buy`**, **`ValueRange`** from **Net_L** / **Net_U** (same style as the PDF), or bracket-style from **Midpoint** if needed.\n- If **Midpoint** **<** **0**: **`Action`** = **`sell`**, **`Midpoint`** = **|Midpoint|**, **`ValueRange`** from absolute net bounds (ascending order) or from **Midpoint**.\n\n**`TransactionDate`**: **Earliest** date in the group when dates differ.\n\n**Metadata** (`DocID`, `FilingDate`, `FilingType`, `Year`): pass through from **Inputs** unchanged on every output object.\n\n## Final Output Instructions:\n- Output **ONLY** valid JSON. No explanations, no markdown, no extra text.\n- Output a JSON array of objects. Each item must follow this exact structure:\n  `{\n    \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n    \"AssetType\": \"(String) ST for stock, EF for ETF, MF for mutual fund\",\n    \"Action\": \"(String) buy, sell, or close\",\n    \"ValueRange\": \"(String) disclosure dollar range from the PDF, or combined after aggregation — for Action close, from the full-exit S row(s); for buy or sell, from net buys minus partial sells when the group has no close\",\n    \"Midpoint\": \"(Number) whole-dollar midpoint of ValueRange — for Action close, from the close row(s); for buy or sell, (Net_L + Net_U) / 2 after netting, or absolute value when Action is sell\",\n    \"TransactionDate\": \"(String) date as written e.g. 01/15/2024\",\n    \"DocID\": \"(String) pass through from Inputs\",\n    \"FilingDate\": \"(String) pass through from Inputs\",\n    \"FilingType\": \"(String) pass through from Inputs\",\n    \"Year\": \"(String) pass through from Inputs\"\n  }`"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        2624,
        2304
      ],
      "id": "0536d80f-f32e-4bff-8059-c80aee6e0034",
      "name": "Extract from PTR (Vision Fallback)",
      "credentials": {
        "openAiApi": {
          "id": "ibSukDIPxiMIGoLl",
          "name": "OpenAI"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const member = $('Congress Trigger').first().json;\nconst multiplier = parseFloat($('Congress Trigger').first().json.multiplier);\nlet strategyValue = parseFloat($('Get Strategy Value').first().json.response[0].strategy_value);\nconst PortfolioValue = multiplier * strategyValue;\n\nreturn $input.all().map(item => {\n  // Extract clean text + parse JSON in one go\n  const raw = item.json.output;\n  let text = '[]';\n  if (Array.isArray(raw) && raw[0]?.content) {\n    text = raw[0].content.find(c => c.type === 'output_text')?.text || '[]';\n  } else if (typeof raw === 'string') text = raw;\n\n  let transactions = [];\n  let metadata = {DocID: member.doc_id, FilingDate: member.filed_at, FilingType: member.report_type};\n  try {\n    const data = JSON.parse(text);\n    transactions = Array.isArray(data) ? data : [];\n    if (transactions[0]) {\n      const first = transactions[0];\n      metadata = {\n        ...metadata,\n        ...(first.DocID ? {DocID: first.DocID} : {}),\n        ...(first.FilingDate ? {FilingDate: first.FilingDate} : {}),\n        ...(first.FilingType ? {FilingType: first.FilingType} : {})\n      };\n    }\n  } catch (e) {\n    console.log('JSON parse error in holdings node:', e);\n  }\n\n  // \"5/15/2025\" → UTC Date\n  const filingDate = metadata.FilingDate\n    ? (() => {\n        const [m, d, y] = metadata.FilingDate.split('/').map(Number);\n        return m && d && y ? new Date(Date.UTC(y, m - 1, d)) : null;\n      })()\n    : null;\n\n  // Single-pass: deduplicate + group by Symbol (sum Midpoints, prefer 'ST')\n  const grouped = transactions.reduce((acc, t) => {\n    if (!t.Symbol || !t.AssetType || !t.ValueRange || typeof t.Midpoint !== 'number') return acc;\n\n    const key = `${t.Symbol}|${t.AssetType}|${t.ValueRange}`;\n    if (acc.seen.has(key)) return acc;\n    acc.seen.add(key);\n\n    const midpoint = parseFloat(String(t.Midpoint).replace(/[$,\\s]/g, '')) || 0;\n    if (midpoint <= 0) return acc;\n\n    if (!acc.result[t.Symbol]) {\n      acc.result[t.Symbol] = {\n        Symbol: t.Symbol,\n        AssetType: t.AssetType,\n        Action: t.Action,\n        ValueRange: t.ValueRange,\n        TransactionDate: filingDate,\n        Midpoint: midpoint\n      };\n    } else {\n      acc.result[t.Symbol].Midpoint += midpoint;\n      if (['ST', 'EF'].includes(t.AssetType)) acc.result[t.Symbol].AssetType = t.AssetType;\n    }\n    return acc;\n  }, { seen: new Set(), result: {} }).result;\n\n  // Final holdings (rounded + percent)\n  const holdings = Object.values(grouped).map(p => ({\n    ...p,\n    Midpoint: Math.round(p.Midpoint),\n    Percent: PortfolioValue > 0 ? parseFloat((p.Midpoint / PortfolioValue).toFixed(6)) : 0\n  }))\n  .filter(p => ['ST', 'EF'].includes(p.AssetType));\n\n  return {\n    json: {\n      congress_report_id: Math.random().toString(36).slice(2, 14),\n      congress_member_id: member.congress_member_id || '',\n      strategy_id: member.strategy_id,\n      first: member.first || '',\n      last: member.last || '',\n      doc_id: metadata.DocID,\n      multiplier: multiplier,\n      filed_at: filingDate,\n      report_type: metadata.FilingType,\n      holdings: JSON.stringify(holdings)\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3056,
        2128
      ],
      "id": "96098e50-448f-40ab-879f-b6c15229d40a",
      "name": "Normalize PTR Output"
    },
    {
      "parameters": {},
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [
        2752,
        3424
      ],
      "id": "c14feb9f-a441-4cfa-8860-c8878c4f0dc1",
      "name": "Calculator3"
    },
    {
      "parameters": {
        "content": "## 🤖 FD AI Extraction & Positions Calculation\n",
        "height": 1088,
        "width": 784,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2480,
        2720
      ],
      "typeVersion": 1,
      "id": "123a5ed2-8c81-474a-b74e-27acdfb98a26",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=# Input\n- DocID: {{ $('Congress Trigger').item.json.doc_id }}\n- FilingDate: {{ $('Congress Trigger').item.json.filed_at }}\n- FilingType: {{ $('Congress Trigger').item.json.report_type }}\n- Year: {{ $('Congress Trigger').item.json.year }}\n\n# FD Report\n{{ $json.text }}\n\nPTR Reports:\n```json\n{{ $('Combine PTR Holdings').item.json.reports }}\n```",
        "options": {
          "systemMessage": "You are a combined financial disclosure and portfolio reconciliation agent. Your job has two phases:\n\n1. **Extract** baseline Federal Disclosure (FD) holdings from the member’s Financial Disclosure Report PDF (Schedule A only), including value ranges, midpoints, and document metadata (DocID, filing fields, year).\n2. **Reconcile** by applying all Periodic Transaction Report (PTR) trades to that baseline. After buys/sells/closes, recalculate standard disclosure value ranges and midpoints so each position matches the same JSON shape as an FD extract.\n\n---\n\n## Input (full)\n\nThe user provides everything below in one turn.\n\n### Phase 1 — FD extraction inputs\n\n#### Inputs (metadata)\n- **DocID**: (String) The ID of the FD document  \n- **FilingDate**: (String) The date of the FD document filing  \n- **FilingType**: (String) Represents FD document type  \n- **Year**: (String) Year of the filing date  \n\n#### Data\nA congressional Financial Disclosure Report (FDR) PDF filed with the Clerk of the House of Representatives. The report contains:\n\n- **Filer Information**: Name, Status, State/District  \n- **Filing Information**: Filing Type, Filing Year, Filing Date  \n- **Schedule A** — Assets and \"Unearned\" Income: A table of assets with columns: Asset, Owner (SP/JT), Value of Asset (dollar range), Income Type(s), Income (dollar range), Tx. > $1,000?. Each asset name includes a type code in brackets (e.g. [ST], [OP], [RP], [BA], [OL], [AB], [MF], [PS]) and may include a ticker symbol in parentheses (e.g. (AAPL), (MSFT)). Some assets have additional Location and Description lines beneath them.  \n- **Schedule B** — Transactions (ignore for Phase 1)  \n- **Schedule C** — Earned Income (ignore)  \n- **Schedule D** — Liabilities (ignore)  \n- **Schedules E–I** — Positions, Agreements, Gifts, Travel, Payments (ignore)  \n\n### Phase 2 — PTR holdings\n\n**PTR Holdings** — A JSON array of arrays. Each inner array contains transaction objects from a single Periodic Transaction Report, representing trades made **after** the FD holdings snapshot. These transactions describe buys, sells, and closes that modify the base FD portfolio over time.\n\nEach transaction object has this structure:\n\n```json\n{\n  \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n  \"AssetType\": \"(String) ST for stock, EF for ETF, MF for mutual fund\",\n  \"Action\": \"(String) buy, sell, or close\",\n  \"ValueRange\": \"(String) exact range as written e.g. $1,001 - $15,000\",\n  \"Midpoint\": \"(Number) midpoint of the value range e.g. 8000\",\n  \"Percent\": \"(Number) IGNORE this field — it was calculated upstream with a different denominator and is not reliable for reconciliation\",\n  \"TransactionDate\": \"(String) date as written MM/DD/YYYY e.g. 01/15/2024\"\n}\n```\n\n---\n\n## Phase 1 rules — Extract FD holdings from Schedule A\n\n- Extract **only** from **Schedule A** (Assets and \"Unearned\" Income).  \n- Do **not** extract from Schedule B (Transactions), Schedule C, D, or any other section.  \n- Include **only** assets that have a ticker symbol in parentheses, e.g. (AAPL), (MSFT), (GOOGL).  \n- Extract only these asset type codes: **[ST]** (stocks), **[EF]** (ETFs), **[MF]** (mutual funds).  \n- Ignore all other asset type codes: [RP] (real property), [BA] (bank accounts), [OL] (partnerships/LLCs), [AB] (other assets), [PS] (private stock), [OP] (options).  \n- Skip any asset where Value of Asset is \"None\" or missing.  \n- If the same symbol+type appears multiple times, include only once using the **largest** value of Asset.  \n- For mutual funds **without** a ticker symbol in parentheses, skip them (e.g. \"Matthews International Mutual Fund [MF]\" has no ticker — skip it).  \n- **Cash position**: After extracting all ticker-qualified [ST]/[EF]/[MF] rows, calculate cash from the **other** asset types that were otherwise ignored ([RP], [BA], [OL], [AB], [PS], [OP]). Sum their midpoints and add a final entry with Symbol `CASH`, AssetType `CA`, ValueRange set to the total as a whole number string (e.g. `\"$156,000\"`), and Midpoint set to that same total as a number. **Do not** include [ST], [EF], or [MF] assets in the cash total — only the non-stock-schedule asset types listed here.  \n\n### Phase 1 internal result (use as “FD Holdings” for Phase 2)\n\nAfter Phase 1, you have a baseline portfolio array. Each object **must** match:\n\n`{\n  \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n  \"AssetType\": \"(String) ST for stock, OP for options, EF for ETF, MF for mutual fund, CA for cash\",\n  \"ValueRange\": \"(String) exact range as written e.g. $1,001 - $15,000\",\n  \"Midpoint\": \"(Number) midpoint of the value range e.g. $8,000\",\n  \"DocID\": \"(String) pass through from Inputs\",\n  \"FilingDate\": \"(String) pass through from Inputs\",\n  \"FilingType\": \"(String) pass through from Inputs\",\n  \"Year\": \"(String) pass through from Inputs\"\n}`\n\n(Only ST, EF, MF, and CA rows appear in practice; the AssetType description matches the FD extractor schema.)\n\nTreat this array as the **FD Holdings** for reconciliation. These holdings reflect the portfolio as of the end of the prior filing year — they are **not** the member’s current holdings until PTRs are applied.\n\n---\n\n## Phase 2 rules — Reconcile FD baseline with PTR transactions\n\nUse the Phase 1 array as the base portfolio. **Metadata**: Every position must carry **`DocID`**, **`FilingDate`**, **`FilingType`**, and **`Year`** copied from **Inputs** for the whole run. Positions created only from a PTR **`buy`** (no prior FD row for that Symbol + AssetType) must use the **same** Input metadata as all other rows. Arithmetic uses **Symbol**, **AssetType**, **Midpoint**, and (when creating a new row mid-run) transaction fields as described below — **not** `Percent` on PTR rows (ignore it).\n\n### Step 1 — Initialize the portfolio\n\n- Start with the FD Holdings array from Phase 1. Each position is keyed by **Symbol + AssetType**.  \n- The CASH entry (AssetType `CA`) will be adjusted dynamically as transactions are applied.  \n\n### Step 2 — Flatten and sort PTR transactions\n\n- Flatten all inner PTR arrays into a single list of transactions.  \n- Sort all transactions by `TransactionDate` in ascending (oldest first) chronological order.  \n\n### Step 3 — Apply each transaction to the portfolio  \n\nProcess each PTR transaction in chronological order:\n\n- **`buy`**:  \n  - If Symbol + AssetType already exists, **add** the transaction’s Midpoint to that position’s Midpoint.  \n  - If it does **not** exist, **create** a new position: set **Symbol**, **AssetType**, interim **Midpoint** from the transaction, and set **DocID**, **FilingDate**, **FilingType**, **Year** from **Inputs** (same as the FD document). Do not rely on the transaction’s **ValueRange** as the final string; **ValueRange** will be replaced in Step 4 from the reconciled Midpoint.  \n  - **Subtract** the transaction’s Midpoint from the CASH position (cash spent).  \n\n- **`sell`**:  \n  - **Subtract** the transaction’s Midpoint from the existing position’s Midpoint.  \n  - If the resulting Midpoint is zero or negative, **remove** the position entirely.  \n  - **Add** the transaction’s Midpoint to CASH (proceeds).  \n\n- **`close`**:  \n  - **Add** the position’s **current** Midpoint to CASH **before** removing the position (full position liquidated at book midpoint).  \n  - **Remove** the position (Symbol + AssetType) entirely, regardless of the PTR row’s Midpoint.  \n\n### Step 4 — Recalculate value ranges and format `ValueRange` like FD output  \n\nAfter all transactions are applied, derive each remaining position’s **`ValueRange`** and final **`Midpoint`** as follows (this replaces any prior PDF- or PTR-sourced range text):\n\n- **ST, EF, MF**: Map the final **`Midpoint`** to **one** standard disclosure bracket string (same style as disclosure PDFs):\n\n  - `$1 - $1,000`  \n  - `$1,001 - $15,000`  \n  - `$15,001 - $50,000`  \n  - `$50,001 - $100,000`  \n  - `$100,001 - $250,000`  \n  - `$250,001 - $500,000`  \n  - `$500,001 - $1,000,000`  \n  - `$1,000,001 - $5,000,000`  \n  - `$5,000,001 - $25,000,000`  \n  - `$25,000,001 - $50,000,000`  \n  - `Over $50,000,000` → use when Midpoint is above 50,000,000  \n\n  Set **`ValueRange`** to that bracket’s label.  \n\n- **CASH** (`CA`): Match **FD_AGENT** cash formatting — set **`ValueRange`** to the final cash **`Midpoint`** as a **whole-dollar string** with thousands separators (e.g. `\"$156,000\"`). If final CASH Midpoint is **0**, set **`ValueRange`** to **`$0`**. Do **not** use the multi-tier bracket strings for CASH.\n\n### Additional rules (Phase 2)\n\n- CASH (`CA`) is updated by every transaction: buys decrease it; sells and closes increase it.  \n- If CASH Midpoint would be zero or negative, set it to **0**; **do not** remove the CASH row.  \n- If a **sell** or **close** references Symbol + AssetType **not** in the portfolio, **skip** that transaction.  \n- Remove any position whose final Midpoint is zero or less from the output (except **keep** CASH even at 0 Midpoint per above).  \n\n---\n\n## Final output instructions\n\n- Output **only** valid JSON. No explanations, no markdown, no extra text.  \n- Output a **JSON array** of objects. The schema is the **same** as **FD_AGENT** final output (reconciled **Midpoint** and disclosure-bracket **ValueRange** from Step 4; metadata from **Inputs**). Each item **must** follow this exact structure:\n\n`{\n  \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n  \"AssetType\": \"(String) ST for stock, OP for options, EF for ETF, MF for mutual fund, CA for cash\",\n  \"ValueRange\": \"(String) exact range as written e.g. $1,001 - $15,000\",\n  \"Midpoint\": \"(Number) midpoint of the value range e.g. $8,000\",\n  \"DocID\": \"(String) pass through from Inputs\",\n  \"FilingDate\": \"(String) pass through from Inputs\",\n  \"FilingType\": \"(String) pass through from Inputs\",\n  \"Year\": \"(String) pass through from Inputs\"\n}`\n\nThis array is the **reconciled current portfolio** after FD extraction and PTR application, in the same shape consumers expect from the standalone FD extractor.\n"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2624,
        2864
      ],
      "id": "6554f77d-6070-4caf-85f1-976527ed088b",
      "name": "Extract from FD",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5.4",
          "mode": "list",
          "cachedResultName": "GPT-5.4"
        },
        "responses": {
          "values": [
            {
              "type": "file",
              "fileType": "base64",
              "binaryPropertyName": "={{ $('Download FD PDF').item.binary.data }}",
              "fileName": "Data"
            },
            {
              "content": "=# Input\n- DocID: {{ $('Congress Trigger').item.json.doc_id }}\n- FilingDate: {{ $('Congress Trigger').item.json.filed_at }}\n- FilingType: {{ $('Congress Trigger').item.json.report_type }}\n- Year: {{ $('Congress Trigger').item.json.year }}\n\nPTR Reports:\n```json\n{{ $('Combine PTR Holdings').item.json.reports }}\n```"
            },
            {
              "role": "system",
              "content": "=You are a combined financial disclosure and portfolio reconciliation agent. Your job has two phases:\n\n1. **Extract** baseline Federal Disclosure (FD) holdings from the member’s Financial Disclosure Report PDF (Schedule A only), including value ranges, midpoints, and document metadata (DocID, filing fields, year).\n2. **Reconcile** by applying all Periodic Transaction Report (PTR) trades to that baseline. After buys/sells/closes, recalculate standard disclosure value ranges and midpoints so each position matches the same JSON shape as an FD extract.\n\n---\n\n## Input (full)\n\nThe user provides everything below in one turn.\n\n### Phase 1 — FD extraction inputs\n\n#### Inputs (metadata)\n- **DocID**: (String) The ID of the FD document  \n- **FilingDate**: (String) The date of the FD document filing  \n- **FilingType**: (String) Represents FD document type  \n- **Year**: (String) Year of the filing date  \n\n#### Data\nA congressional Financial Disclosure Report (FDR) PDF filed with the Clerk of the House of Representatives. The report contains:\n\n- **Filer Information**: Name, Status, State/District  \n- **Filing Information**: Filing Type, Filing Year, Filing Date  \n- **Schedule A** — Assets and \"Unearned\" Income: A table of assets with columns: Asset, Owner (SP/JT), Value of Asset (dollar range), Income Type(s), Income (dollar range), Tx. > $1,000?. Each asset name includes a type code in brackets (e.g. [ST], [OP], [RP], [BA], [OL], [AB], [MF], [PS]) and may include a ticker symbol in parentheses (e.g. (AAPL), (MSFT)). Some assets have additional Location and Description lines beneath them.  \n- **Schedule B** — Transactions (ignore for Phase 1)  \n- **Schedule C** — Earned Income (ignore)  \n- **Schedule D** — Liabilities (ignore)  \n- **Schedules E–I** — Positions, Agreements, Gifts, Travel, Payments (ignore)  \n\n### Phase 2 — PTR holdings\n\n**PTR Holdings** — A JSON array of arrays. Each inner array contains transaction objects from a single Periodic Transaction Report, representing trades made **after** the FD holdings snapshot. These transactions describe buys, sells, and closes that modify the base FD portfolio over time.\n\nEach transaction object has this structure:\n\n```json\n{\n  \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n  \"AssetType\": \"(String) ST for stock, EF for ETF, MF for mutual fund\",\n  \"Action\": \"(String) buy, sell, or close\",\n  \"ValueRange\": \"(String) exact range as written e.g. $1,001 - $15,000\",\n  \"Midpoint\": \"(Number) midpoint of the value range e.g. 8000\",\n  \"Percent\": \"(Number) IGNORE this field — it was calculated upstream with a different denominator and is not reliable for reconciliation\",\n  \"TransactionDate\": \"(String) date as written MM/DD/YYYY e.g. 01/15/2024\"\n}\n```\n\n---\n\n## Phase 1 rules — Extract FD holdings from Schedule A\n\n- Extract **only** from **Schedule A** (Assets and \"Unearned\" Income).  \n- Do **not** extract from Schedule B (Transactions), Schedule C, D, or any other section.  \n- Include **only** assets that have a ticker symbol in parentheses, e.g. (AAPL), (MSFT), (GOOGL).  \n- Extract only these asset type codes: **[ST]** (stocks), **[EF]** (ETFs), **[MF]** (mutual funds).  \n- Ignore all other asset type codes: [RP] (real property), [BA] (bank accounts), [OL] (partnerships/LLCs), [AB] (other assets), [PS] (private stock), [OP] (options).  \n- Skip any asset where Value of Asset is \"None\" or missing.  \n- If the same symbol+type appears multiple times, include only once using the **largest** value of Asset.  \n- For mutual funds **without** a ticker symbol in parentheses, skip them (e.g. \"Matthews International Mutual Fund [MF]\" has no ticker — skip it).  \n- **Cash position**: After extracting all ticker-qualified [ST]/[EF]/[MF] rows, calculate cash from the **other** asset types that were otherwise ignored ([RP], [BA], [OL], [AB], [PS], [OP]). Sum their midpoints and add a final entry with Symbol `CASH`, AssetType `CA`, ValueRange set to the total as a whole number string (e.g. `\"$156,000\"`), and Midpoint set to that same total as a number. **Do not** include [ST], [EF], or [MF] assets in the cash total — only the non-stock-schedule asset types listed here.  \n\n### Phase 1 internal result (use as “FD Holdings” for Phase 2)\n\nAfter Phase 1, you have a baseline portfolio array. Each object **must** match:\n\n`{\n  \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n  \"AssetType\": \"(String) ST for stock, OP for options, EF for ETF, MF for mutual fund, CA for cash\",\n  \"ValueRange\": \"(String) exact range as written e.g. $1,001 - $15,000\",\n  \"Midpoint\": \"(Number) midpoint of the value range e.g. $8,000\",\n  \"DocID\": \"(String) pass through from Inputs\",\n  \"FilingDate\": \"(String) pass through from Inputs\",\n  \"FilingType\": \"(String) pass through from Inputs\",\n  \"Year\": \"(String) pass through from Inputs\"\n}`\n\n(Only ST, EF, MF, and CA rows appear in practice; the AssetType description matches the FD extractor schema.)\n\nTreat this array as the **FD Holdings** for reconciliation. These holdings reflect the portfolio as of the end of the prior filing year — they are **not** the member’s current holdings until PTRs are applied.\n\n---\n\n## Phase 2 rules — Reconcile FD baseline with PTR transactions\n\nUse the Phase 1 array as the base portfolio. **Metadata**: Every position must carry **`DocID`**, **`FilingDate`**, **`FilingType`**, and **`Year`** copied from **Inputs** for the whole run. Positions created only from a PTR **`buy`** (no prior FD row for that Symbol + AssetType) must use the **same** Input metadata as all other rows. Arithmetic uses **Symbol**, **AssetType**, **Midpoint**, and (when creating a new row mid-run) transaction fields as described below — **not** `Percent` on PTR rows (ignore it).\n\n### Step 1 — Initialize the portfolio\n\n- Start with the FD Holdings array from Phase 1. Each position is keyed by **Symbol + AssetType**.  \n- The CASH entry (AssetType `CA`) will be adjusted dynamically as transactions are applied.  \n\n### Step 2 — Flatten and sort PTR transactions\n\n- Flatten all inner PTR arrays into a single list of transactions.  \n- Sort all transactions by `TransactionDate` in ascending (oldest first) chronological order.  \n\n### Step 3 — Apply each transaction to the portfolio  \n\nProcess each PTR transaction in chronological order:\n\n- **`buy`**:  \n  - If Symbol + AssetType already exists, **add** the transaction’s Midpoint to that position’s Midpoint.  \n  - If it does **not** exist, **create** a new position: set **Symbol**, **AssetType**, interim **Midpoint** from the transaction, and set **DocID**, **FilingDate**, **FilingType**, **Year** from **Inputs** (same as the FD document). Do not rely on the transaction’s **ValueRange** as the final string; **ValueRange** will be replaced in Step 4 from the reconciled Midpoint.  \n  - **Subtract** the transaction’s Midpoint from the CASH position (cash spent).  \n\n- **`sell`**:  \n  - **Subtract** the transaction’s Midpoint from the existing position’s Midpoint.  \n  - If the resulting Midpoint is zero or negative, **remove** the position entirely.  \n  - **Add** the transaction’s Midpoint to CASH (proceeds).  \n\n- **`close`**:  \n  - **Add** the position’s **current** Midpoint to CASH **before** removing the position (full position liquidated at book midpoint).  \n  - **Remove** the position (Symbol + AssetType) entirely, regardless of the PTR row’s Midpoint.  \n\n### Step 4 — Recalculate value ranges and format `ValueRange` like FD output  \n\nAfter all transactions are applied, derive each remaining position’s **`ValueRange`** and final **`Midpoint`** as follows (this replaces any prior PDF- or PTR-sourced range text):\n\n- **ST, EF, MF**: Map the final **`Midpoint`** to **one** standard disclosure bracket string (same style as disclosure PDFs):\n\n  - `$1 - $1,000`  \n  - `$1,001 - $15,000`  \n  - `$15,001 - $50,000`  \n  - `$50,001 - $100,000`  \n  - `$100,001 - $250,000`  \n  - `$250,001 - $500,000`  \n  - `$500,001 - $1,000,000`  \n  - `$1,000,001 - $5,000,000`  \n  - `$5,000,001 - $25,000,000`  \n  - `$25,000,001 - $50,000,000`  \n  - `Over $50,000,000` → use when Midpoint is above 50,000,000  \n\n  Set **`ValueRange`** to that bracket’s label.  \n\n- **CASH** (`CA`): Match **FD_AGENT** cash formatting — set **`ValueRange`** to the final cash **`Midpoint`** as a **whole-dollar string** with thousands separators (e.g. `\"$156,000\"`). If final CASH Midpoint is **0**, set **`ValueRange`** to **`$0`**. Do **not** use the multi-tier bracket strings for CASH.\n\n### Additional rules (Phase 2)\n\n- CASH (`CA`) is updated by every transaction: buys decrease it; sells and closes increase it.  \n- If CASH Midpoint would be zero or negative, set it to **0**; **do not** remove the CASH row.  \n- If a **sell** or **close** references Symbol + AssetType **not** in the portfolio, **skip** that transaction.  \n- Remove any position whose final Midpoint is zero or less from the output (except **keep** CASH even at 0 Midpoint per above).  \n\n---\n\n## Final output instructions\n\n- Output **only** valid JSON. No explanations, no markdown, no extra text.  \n- Output a **JSON array** of objects. The schema is the **same** as **FD_AGENT** final output (reconciled **Midpoint** and disclosure-bracket **ValueRange** from Step 4; metadata from **Inputs**). Each item **must** follow this exact structure:\n\n`{\n  \"Symbol\": \"(String) ticker symbol e.g. AAPL\",\n  \"AssetType\": \"(String) ST for stock, OP for options, EF for ETF, MF for mutual fund, CA for cash\",\n  \"ValueRange\": \"(String) exact range as written e.g. $1,001 - $15,000\",\n  \"Midpoint\": \"(Number) midpoint of the value range e.g. $8,000\",\n  \"DocID\": \"(String) pass through from Inputs\",\n  \"FilingDate\": \"(String) pass through from Inputs\",\n  \"FilingType\": \"(String) pass through from Inputs\",\n  \"Year\": \"(String) pass through from Inputs\"\n}`\n\nThis array is the **reconciled current portfolio** after FD extraction and PTR application, in the same shape consumers expect from the standalone FD extractor.\n"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        2608,
        3216
      ],
      "id": "21f1d30e-b84a-479b-ab22-b9b99695683f",
      "name": "Extract from FD (Vision Fallback)",
      "credentials": {
        "openAiApi": {
          "id": "ibSukDIPxiMIGoLl",
          "name": "OpenAI"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const member = $('Congress Trigger').first().json;\nlet strategyValue = parseFloat($('Get Strategy Value').first().json.response[0].strategy_value);\n\nreturn $input.all().map(item => {\n  // Extract clean text from LLM output\n  const raw = item.json.output;\n  let text = '[]';\n  if (Array.isArray(raw) && raw[0]?.content) {\n    text = raw[0].content.find(c => c.type === 'output_text')?.text || '[]';\n  } else if (typeof raw === 'string') {\n    text = raw;\n  }\n\n  // Parse transactions + metadata from first object\n  let transactions = [];\n  let metadata = {DocID: member.doc_id, FilingDate: member.filed_at, FilingType: member.report_type};\n  try {\n    const data = JSON.parse(text);\n    transactions = Array.isArray(data) ? data : [];\n    if (transactions[0]) {\n      const first = transactions[0];\n      metadata = {\n        ...metadata,\n        ...(first.DocID ? {DocID: first.DocID} : {}),\n        ...(first.FilingDate ? {FilingDate: first.FilingDate} : {}),\n        ...(first.FilingType ? {FilingType: first.FilingType} : {})\n      };\n    }\n  } catch (e) {\n    console.log('JSON parse error in holdings node:', e);\n  }\n\n  // Convert \"5/15/2025\" → UTC Date\n  const filingDate = metadata.FilingDate\n    ? (() => {\n        const [m, d, y] = metadata.FilingDate.split('/').map(Number);\n        return m && d && y ? new Date(Date.UTC(y, m - 1, d)) : null;\n      })()\n    : null;\n\n  // Single-pass deduplicate + group by Symbol (sum Midpoints, prefer 'ST')\n  const grouped = transactions.reduce((acc, t) => {\n    if (!t.Symbol || !t.AssetType || !t.ValueRange || typeof t.Midpoint !== 'number') return acc;\n\n    const key = `${t.Symbol}|${t.AssetType}|${t.ValueRange}`;\n    if (acc.seen.has(key)) return acc;\n    acc.seen.add(key);\n\n    const midpoint = parseFloat(String(t.Midpoint).replace(/[$,\\s]/g, '')) || 0;\n    if (midpoint <= 0) return acc;\n\n    if (!acc.result[t.Symbol]) {\n      acc.result[t.Symbol] = {\n        Symbol: t.Symbol,\n        AssetType: t.AssetType,\n        ValueRange: t.ValueRange,\n        FilingDate: filingDate,\n        Midpoint: midpoint\n      };\n    } else {\n      acc.result[t.Symbol].Midpoint += midpoint;\n      if (['ST', 'EF'].includes(t.AssetType)) acc.result[t.Symbol].AssetType = t.AssetType;\n    }\n    return acc;\n  }, { seen: new Set(), result: {} }).result;\n\n  // Round Midpoints\n  const positions = Object.values(grouped).map(p => ({\n    ...p,\n    Midpoint: Math.round(p.Midpoint)\n  }))\n  .filter(p => ['ST', 'EF'].includes(p.AssetType));\n\n  // Calculate PortfolioValue + Multiplier (this version's logic)\n  const PortfolioValue = positions.reduce((sum, p) => sum + p.Midpoint, 0);\n  const Multiplier = strategyValue > 0 ? PortfolioValue / strategyValue : 0;\n\n  // Add Percent\n  const holdings = positions.map(p => ({\n    ...p,\n    Percent: Multiplier > 0 ? parseFloat((p.Midpoint / PortfolioValue).toFixed(6)) : 0\n  }));\n\n  return {\n    json: {\n      congress_report_id: Math.random().toString(36).slice(2, 14),\n      congress_member_id: member.congress_member_id || '',\n      strategy_id: member.strategy_id,\n      first: member.first || '',\n      last: member.last || '',\n      doc_id: metadata.DocID,\n      multiplier: Multiplier,\n      filed_at: filingDate,\n      report_type: metadata.FilingType,\n      holdings: JSON.stringify(holdings)\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3040,
        3056
      ],
      "id": "3a17da19-045f-4f40-b9d3-3b9eda056189",
      "name": "Normalize FD Output"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "select \"congress_members\".\"congress_member_id\", \"congress_members\".\"strategy_id\", \"congress_members\".\"first\", \"congress_members\".\"last\", \"congress_members\".\"location\",\"congress_members\".\"multiplier\", ((select COALESCE(JSONB_AGG(congress_reports.* ORDER BY congress_reports.filed_at DESC), '[]'::jsonb) from \"congress_reports\" where congress_member_id = congress_members.congress_member_id)) AS reports\n  from \"congress_members\"\n  order by \"multiplier\" desc",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1168,
        1200
      ],
      "id": "7d130f6a-6338-4515-b844-e8cf3789c902",
      "name": "Get Members",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "id": "1tho5Ld1N34iwkFW",
          "name": "N8N Postgres"
        }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "congress_members",
          "mode": "list",
          "cachedResultName": "congress_members"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "congress_member_id": "={{ $('Parse Congress Holdings').item.json.congress_member_id }}",
            "updated_at": "={{ new Date().toISOString() }}",
            "multiplier": "={{ $('Parse Congress Holdings').item.json.multiplier }}"
          },
          "matchingColumns": [
            "congress_member_id"
          ],
          "schema": [
            {
              "id": "congress_member_id",
              "displayName": "congress_member_id",
              "required": true,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "first",
              "displayName": "first",
              "required": true,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "last",
              "displayName": "last",
              "required": true,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "multiplier",
              "displayName": "multiplier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "updated_at",
              "displayName": "updated_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "displayName": "created_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "strategy_id",
              "displayName": "strategy_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        3776,
        2720
      ],
      "id": "9452e71d-8cc7-4940-bfe1-3e85da4c79e7",
      "name": "Update Congress Values",
      "credentials": {
        "postgres": {
          "id": "1tho5Ld1N34iwkFW",
          "name": "N8N Postgres"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const members = $('Get Members').all().map(member => member.json).filter(member => member?.congress_member_id);\nconst reports = members.map(member => member.reports.map(report => report.doc_id)).flat();\n\nlet results = [];\n\nfor (const item of $input.all()) {\n  const xmlString = item.json.results;\n  const memberRegex = /<Member>([\\s\\S]*?)<\\/Member>/g;\n  let match;\n\n  while ((match = memberRegex.exec(xmlString)) !== null) {\n    const block = match[1];\n\n    //get all tags\n    const get = (tag) => (block.match(new RegExp(`<${tag}>([^<]*)</${tag}>`)) || [])[1] || '';\n\n    //get first name from xml\n    const firstName = get('First');\n    const lastName = get('Last');\n    if (!firstName || !lastName) continue;\n\n    //find first and last name from the members list that matches the block\n    let foundMember = members.find(member => (\n      member.first.toLowerCase() === firstName.toLowerCase() &&\n      member.last.toLowerCase() === lastName.toLowerCase()\n    ));\n    \n    let member = {\n      first: firstName,\n      last: lastName,\n      location: '',\n      doc_id: get('DocID'),\n      report_type: get('FilingType'),\n      filed_at: get('FilingDate'),\n      year: Number(get('Year'))\n    }\n    if(foundMember) {\n      member = {\n        ...member,\n        congress_member_id: foundMember.congress_member_id,\n        strategy_id: foundMember.strategy_id,\n        multiplier: foundMember.multiplier,\n        first: foundMember.first,\n        last: foundMember.last,\n        location: foundMember.location\n      };\n    }\n    \n    //skip if not FD or PTR report\n    if (!['O','P'].includes(get('FilingType'))) continue;\n    //if (!['P'].includes(get('FilingType'))) continue;\n    //skip if no strategy_id for PTR report\n    if(get('FilingType') === 'P' && !member.strategy_id) continue;\n    //skip if DodID has already been saved\n    if(reports.includes(get('DocID')) || get('DocID') === \"\") continue;\n    \n    results.push(member);\n  }\n}\n\nif(results.length === 0) return [];\n\n//results = results.slice(0, 20);\n\nresults.sort((a, b) => new Date(a.filed_at) - new Date(b.filed_at));\nreturn results.map(r => ({ json: r }));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2064,
        1200
      ],
      "id": "065eee5e-f40a-4ac4-a37e-99fc9480517a",
      "name": "Find Latest Docs",
      "alwaysOutputData": false,
      "executeOnce": false
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO \"congress_reports\" (\"congress_report_id\", \"congress_member_id\", \"doc_id\", \"report_type\", \"holdings\", \"filed_at\")\nVALUES (\n  '{{ $('Parse Congress Holdings').item.json.congress_report_id }}',\n  '{{ $('Parse Congress Holdings').item.json.congress_member_id }}',\n  '{{ $('Parse Congress Holdings').item.json.doc_id }}',\n  '{{ $('Parse Congress Holdings').item.json.report_type }}',\n  '{{ $('Parse Congress Holdings').item.json.holdings }}'::jsonb,\n  '{{ $('Parse Congress Holdings').item.json.filed_at }}'\n)",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        4016,
        2576
      ],
      "id": "49706d92-6c60-4ab2-be15-f7abde8cacb0",
      "name": "Insert Reports",
      "credentials": {
        "postgres": {
          "id": "1tho5Ld1N34iwkFW",
          "name": "N8N Postgres"
        }
      }
    },
    {
      "parameters": {
        "url": "https://alphainsider.com/api/getStrategyValues",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "strategy_id[0]",
              "value": "={{ $('Congress Trigger').item.json.strategy_id }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1168,
        2592
      ],
      "id": "d798259c-4e8a-492a-b8f2-96f83a0390f0",
      "name": "Get Strategy Value",
      "credentials": {
        "httpBearerAuth": {
          "id": "W3JsfsB9EKpjx96N",
          "name": "AlphaInsider Test"
        }
      }
    },
    {
      "parameters": {},
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [
        2768,
        3056
      ],
      "id": "60e6b7bc-74f1-484a-8434-968aa06ae8b2",
      "name": "Calculator4"
    },
    {
      "parameters": {
        "content": "## Get Doc Info & Save",
        "height": 2032,
        "width": 944,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3280,
        1776
      ],
      "typeVersion": 1,
      "id": "05e6a55f-c412-4c0d-91e1-8e08105cf1fb",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Global Settings\n\n## How it works\nCentralizes workflow configuration including strategy ID, security whitelist, session tracking, and message input. All downstream nodes reference these global settings for consistent execution context.\n\n## Setup steps\n- **(Required)** Set strategy_id by copying it from your AlphaInsider strategy URL (e.g., \"niAlE-cMI8TdsYQllZLmf\")\n- **(Optional)** Set whitelist array with \"STOCK:EXCHANGE\" format to restrict tradeable securities, or leave as [] for no restrictions",
        "height": 2032,
        "width": 640,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4240,
        1776
      ],
      "typeVersion": 1,
      "id": "e3e0883a-d88c-4f6e-ba9f-769446f8f492",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cc0b4d35-1e3e-416a-8891-2fcd9269cec2",
              "name": "session_id",
              "value": "={{ $('Parse Congress Holdings').item.json.congress_member_id }}",
              "type": "string"
            },
            {
              "id": "8655d162-6ff5-484d-a5be-0800b06a4e1b",
              "name": "input",
              "value": "={{ $('Parse Congress Holdings').item.json.input }}",
              "type": "string"
            },
            {
              "id": "a1540549-e689-4756-8e68-982f5f64066c",
              "name": "strategy_id",
              "value": "={{ $('Parse Congress Holdings').item.json.strategy_id }}",
              "type": "string"
            },
            {
              "id": "a2782249-f222-47c4-8a1b-1ae4c04b7e34",
              "name": "position_mode",
              "value": "={{ $('Parse Congress Holdings').item.json.report_type === 'O' ? 'replace' : 'adjust' }}",
              "type": "string"
            },
            {
              "id": "9023a0a4-2ab6-4680-b55c-708e55647da8",
              "name": "whitelist",
              "value": "[]",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4656,
        2576
      ],
      "id": "bc17f62f-c5e4-40be-aa7f-b1b154eca110",
      "name": "Global Settings"
    },
    {
      "parameters": {
        "content": "## AlphaInsider Position Manager\n\n## How it works\nFetches current portfolio positions from AlphaInsider, calculates allocation percentages with leverage, then uses OpenAI GPT-5.2-pro to analyze trading signals and route to three actions: execute trades, create audience posts, or provide direct Q&A responses.\n\n## Setup steps\n- **(Required)** Get AlphaInsider API token from developer settings (click n8n button). Add to \"Get Positions\", \"Create Post\", and \"Create Orders\" nodes\n- **(Required)** Get OpenAI API key and add to \"OpenAI Model\" and \"Transcribe Voice Message\" nodes",
        "height": 2032,
        "width": 2032,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4896,
        1776
      ],
      "typeVersion": 1,
      "id": "2f2b8bb8-2df9-404c-8633-5504de87a58a",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "jsCode": "// Get http response input from AI\nconst positions = $input.first().json.response;\n\ntry {\n  // Process positions to calculate percentages\n  if (positions && Array.isArray(positions)) {\n    // Calculate total strategy value from positions\n    let strategyValue = 0;\n    for (const position of positions) {\n      const amount = parseFloat(position.amount);\n      const isAsset = amount >= 0;\n      const currentPrice = parseFloat(isAsset ? position.bid : position.ask);\n      strategyValue += amount * currentPrice;\n    }\n\n    if(strategyValue <= 0) throw new Error('Strategy value must be greater than 0.');\n\n    // Calculate percentage for each position\n    const filteredPositions = positions.filter((position) => `${position.stock}:${position.provider}`.toUpperCase() !== 'USD:ALPHAINSIDER');\n    const parsedPositions = filteredPositions.map((position) => {\n      const amount = parseFloat(position.amount);\n      const isAsset = amount >= 0;\n      const currentPrice = parseFloat(isAsset ? position.bid : position.ask);\n      const positionPercent = Math.abs((amount * currentPrice) / strategyValue);\n\n      return {\n        stock_id: position.stock_id,\n        symbol: position.symbol,\n        stock: position.stock,\n        exchange: position.exchange,\n        action: (isAsset ? 'long' : 'short'),\n        percent: positionPercent\n      };\n    });\n\n    //return parsed positions\n    return {positions: JSON.stringify(parsedPositions)};\n  }\n  \n  return {positions: []};\n  \n} catch (error) {\n  throw {\n    error: 'Failed to fetch positions',\n    message: error.message,\n    input: positions\n  };\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5168,
        2576
      ],
      "id": "7955f507-302f-440a-83cb-5b75baec8d0f",
      "name": "Parse Position Percents",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Input:\n{{ $('Global Settings').item.json.input }}\n\nWhitelist:\n{{ $('Global Settings').item.json.whitelist }}\n\nPosition Mode:\n{{ $('Global Settings').item.json.position_mode }}\n\nPositions:\n```json\n{{ $json.positions }}\n```",
        "options": {
          "systemMessage": "### Inputs:\n- Input: [Text]\n- Whitelist (ONLY these securities are allowed): [whitelist]\n  - Whitelist entries are plain ticker **symbols** (e.g. `[\"TSLA\", \"BTC-USD\", \"SPY\"]`) for user convenience. They are matched against the `symbol` field from the Positions input or the Search Stocks response. An empty array `[]` means no restrictions.\n- Position Mode: [adjust/replace]  \n  - `adjust` (default): Only modify positions explicitly mentioned in the input. **All unmentioned existing positions MUST be carried forward unchanged** into the allocations output. This prevents accidental liquidation of holdings the user didn't intend to change.  \n  - `replace`: The output allocations represent the COMPLETE desired portfolio. Any existing positions NOT included in the output will be closed. Use this when the user wants to define their entire portfolio from scratch.\n- Positions: [JSON array of objects]\n\n### Rules (Critical):\n\n**STOCK_ID FORMAT — CRITICAL NON-NEGOTIABLE RULE (HIGHEST PRIORITY)**  \nThe `stock_id` field in **EVERY** allocation object **MUST** be the internal AlphaInsider `stock_id` — the opaque identifier returned by the Search Stocks tool, or the value already present in the Positions input. Example: `--7w5ouKQ6mb6LoXqhyn7`.  \n- **NEVER** construct a `SYMBOL` string (e.g. `TSLA`, `BTC-USD`) for the `stock_id` field in allocations. That format is brittle — tickers and exchange listings can change, which would break orders.  \n- The internal `stock_id` is the **single source of truth** for creating orders on AlphaInsider.  \n- **Lookup rule**: For each security the user wants to trade, first check the Positions input. If a position's `symbol` (or `stock`) matches the security the user referenced, reuse that position's `stock_id` verbatim. **If the new order given by the user does not have a match with the given Positions input, lookup the `stock_id` using the Search Stocks tool.** Skip any security Search Stocks cannot find.  \n- This rule is absolute. Do not guess, do not concatenate, do not reformat.\n\n**ACTION TYPES:**  \n1. **action = 'trade'**: User intends to make a trade. Populate allocations array with trade information.  \n2. **action = 'post'**: User is making a post to their audience (no trade intention) OR user is directly asking the agent to make a post on their behalf. Populate the `post` object with:  \n   - **User-authored posts**: `description` contains exact user message, optional `url` for main URL  \n   - **Agent-generated posts**: `description` contains agent-created technical analysis or commentary based on user's request and current market/position information  \n3. **action = 'none'**: User is asking questions directly or input is unrelated/not useful. Answer questions in `message` or explain why no action was taken.\n\n**TRADING RULES (when action = 'trade'):**  \n1. **Whitelist handling**: If the whitelist input is non-empty, ONLY include securities whose `symbol` (from Positions input or Search Stocks response) appears in the whitelist. Whitelist entries are plain ticker symbols such as `\"TSLA\"`, `\"BTC-USD\"`, `\"SPY\"`. The `stock_id` written into the allocation is always the internal AlphaInsider `stock_id` — never a whitelist entry. If the whitelist is empty, there are no symbol restrictions (but Rule #8 still applies).\n- **CRITICAL**: When the input mentions a security that is not already in the Positions input, you MUST use the Search Stocks tool to retrieve the authoritative `stock_id` before including it in allocations. If a security is not found on AlphaInsider, skip it — do NOT include it in allocations.  \n2. Positions input is a JSON array of currently held percentage allocation of stock/cryptocurrency positions. Positions will never contain both cryptocurrency and stock positions; only one type or the other. Each Positions object has these fields:  \n- `stock_id`: Internal AlphaInsider identifier (opaque string, e.g. `--7w5ouKQ6mb6LoXqhyn7`). Use **as-is** when carrying forward or referring to an existing position in allocations.  \n- `symbol`: The trading symbol (e.g. `TSLA`, `BTC-USD`). Primary key for whitelist matching and for matching user text references.  \n- `stock`: The asset ticker (e.g. `TSLA`, `BTC`). Also useful for matching user text references.  \n- `exchange`: The exchange name (e.g. `NASDAQ`, `COINBASE`). For optional human-readable context only.  \n- `action`: The current direction the stock is being held (`long` or `short`).  \n- `percent`: 0 < x <= 2; decimal with leverage. If x <= 1, no leverage. 1 < x <= 2, leverage up to 200%.  \n- **CRITICAL - POSITION MODE HANDLING**: The `Position Mode` input determines how to handle existing positions when creating trade allocations:  \n  - **When Position Mode = `adjust`** (this is the default):  \n    - **You MUST carry forward ALL existing positions that are NOT explicitly mentioned in the input text.** Copy them into the allocations output using the same `stock_id` (internal AlphaInsider ID), `action`, and `percent` as they appear in the Positions input.  \n    - Only add, modify, or remove positions that the input text specifically references.  \n    - If the user says \"Buy TSLA at 50%\" and currently holds BTC and ETH positions, the output allocations MUST include the new TSLA position AND the existing BTC and ETH positions unchanged (with their original internal `stock_id` values).  \n    - Only close/remove a position if the input explicitly signals closing that specific security (e.g., \"sell my BTC\", \"close TSLA\", \"exit ETH\"). Match user references to existing positions via the position's `symbol` or `stock` field — not the opaque `stock_id`.  \n    - This prevents accidental liquidation of holdings the user did not intend to change.  \n  - **When Position Mode = `replace`**:  \n    - The output allocations represent the COMPLETE desired portfolio state.  \n    - Any existing positions NOT included in the output will be closed.  \n    - Only include positions the user wants in the final portfolio.  \n    - Signals like \"close everything\", \"getting out of all positions\", \"start fresh with X\", or \"all in on Y\" naturally fit this mode.  \n  - **In both modes**: Ensure the total percent across all allocations does not exceed 2.0. **The user's current input trade signal always takes highest priority.** If honoring the requested trade AND carrying forward existing positions would exceed 2.0, **proportionally reduce the carried-forward existing positions** (not the new trade) to make room. The agent should redistribute existing holdings so the new trade is fulfilled at its requested size and the total stays at or below 2.0. Explain the redistribution in the message field so the user knows what changed.  \n3. **Resolving security names**: You do NOT maintain a static mapping from common names or tickers to `stock_id` — internal IDs are opaque and cannot be memorized. **For each security mentioned in the input, first check the Positions input for a matching `symbol` or `stock`; if found, reuse that position's `stock_id`. If the new order given by the user does not have a match with the given Positions input, lookup the `stock_id` using the Search Stocks tool** (with the company name or ticker as the `search` parameter — e.g. \"Tesla\", \"TSLA\", \"Bitcoin\", \"BTC\", \"MicroStrategy\"). Use the returned `stock_id` verbatim in the allocation. Skip any security Search Stocks cannot find.  \n4. Be conservative. Only trigger trades when there is a **clear** long, short, or close signal.  \n    - Strong Long: buy, bullish, loading up, moon, buying more, strong buy, undervalued  \n    - Strong Short: sell, dumping, overvalued, take profit, bearish, exiting  \n    - Strong Close: take profit, exiting, holding cash, getting out, trade closed, closing positions.  \n5. **CRITICAL**: Distinguish between different types of user input:  \n   **Trading Signals (action = 'trade')** - Look for ACTION words or INTENT:  \n    - Direct action verbs: \"buying\", \"selling\", \"going long\", \"going short\", \"loading up\", \"dumping\", \"taking profit\", \"closing\", \"exiting\", \"adding\", \"trimming\"  \n    - Intent phrases: \"I'm in\", \"I'm out\", \"getting in\", \"getting out\", \"entering\", \"accumulating\"  \n    - Position statements: \"long on X\", \"short on X\", \"holding X\", \"sold my X\"  \n    - Recommendations with conviction: \"buy X now\", \"time to sell\", \"take profit here\", \"strong buy\", \"strong sell\"  \n    - Combined technical + action: \"RSI oversold, buying\", \"broke resistance, going long\", \"hit stop loss, closing position\"  \n\n   **Posts to Audience (action = 'post')** - User is communicating with their audience OR requesting the agent to post on their behalf:  \n    - **User-authored**: Broadcasting information, opinions, or analysis to followers; sharing news, updates, or commentary; may include URLs to articles, charts, or resources  \n    - **Agent-generated**: User directly asks agent to \"make a post\", \"post about\", \"share an update\", etc. - agent creates technical analysis or commentary based on provided information, current positions, or market data  \n    - No explicit trading intention for themselves in either case  \n    - **CRITICAL** ASCII ONLY: All post content in the `description` field MUST contain only ASCII characters (character codes 0-127). Strip or replace any non-ASCII characters (emojis, special unicode characters, accented letters, etc.) before including in the post. If the entire input is non-ASCII or becomes empty after filtering, set action = 'none' and explain in the message field.  \n\n   **Direct Conversation (action = 'none')** - User talking directly to the agent:  \n    - Pure observation: \"RSI is at 30\", \"MACD crossed above signal\", \"broke resistance at $100\"  \n    - Chart descriptions: \"forming a cup and handle\", \"looks like a bullish pattern\", \"support at $50\"  \n    - Trading-related questions: \"What are my current positions?\", \"What do you think about X stock?\", \"How much am I allocated?\"  \n    - **OFF-TOPIC INPUT**: Questions or statements NOT related to trading, stocks, or cryptocurrency (e.g., \"What's the weather?\", \"What should I do today?\", \"Tell me a joke\", general life advice, entertainment, etc.)  \n    - **CRITICAL**: Do NOT attempt to answer off-topic questions. For any input unrelated to trading/stocks/crypto, respond in the message field with a polite decline stating the agent's scope is limited to trading signals, stock/cryptocurrency analysis, and position management.  \n    - Unclear input that cannot be categorized  \n      **When in doubt, assume it's action = 'none'** - Only set action = 'trade' if at least 50% certain it's a trading signal  \n6. If the signal is weak, neutral, or ambiguous → set action = 'none'  \n7. **IMPORTANT** Percentages: Use decimals between 0 and 2.0 (e.g., 1.5 for 150% of buying power/leverage). The sum of all \"percent\" values across allocations MUST NOT exceed 2.0 (200% or 2x leverage).  \n    - If the user specifies a percentage or allocation size, use EXACTLY what they specify. Do NOT round up to 2.0 or auto-fill to maximum leverage. Examples: \"buy TSLA at 50%\" → use 0.5; \"go long BTC with 75%\" → use 0.75; \"put 30% in NVDA\" → use 0.3.  \n    - If the user specifies other allocation rules (e.g., \"equal weight\", \"no leverage\", \"keep 50% cash\"), follow those rules exactly.  \n    - Only default to higher allocations when the user gives NO specific percentage AND uses strong conviction language (e.g., \"all in\", \"max long\", \"full send\"). Even then, consider current positions before defaulting to 2.0.  \n    - For single asset trades with no specified percentage and strong conviction → use up to 2.0.  \n    - For multiple assets (e.g., selling one to buy another) → split logically and ensure total ≤ 2.0 (e.g., 1 sell + 1 buy).  \n    - Interpret \"selling A to B\" as reallocating from A to B, with balanced percentages (e.g., 1 close A + 1 long B).  \n    - \"sell\" can imply shorting if context suggests; \"buy\" implies going long, \"close\" implies getting out of position and going into cash.  \n    - If close signal is general (not tied to specific assets) and wording is relevant (e.g., \"getting out of everything\" or \"closing all positions\"), set action: 'trade' and allocations: [] to indicate closing all current positions.  \n8. Allocation objects in the allocations array must all be the same security type: either all `security = \"stock\"` or all `security = \"cryptocurrency\"` (as reported by Search Stocks and the Positions input). Do not mix stocks and cryptocurrencies in the same output allocations.  \n9. Handle reallocations: If text implies shifting from one security to another, use equal splits unless the user specifies otherwise. Always honor user-specified percentages, weights, or allocation rules over any default behavior.  \n10. **MESSAGE FIELD**: Always populate the `message` field with notes about what was done:  \n    - For trades (action = 'trade'): Describe what positions have changed and what the current positions are. **CRITICAL**: Always use the human-readable `symbol` (e.g. `TSLA`, `BTC-USD`) for all security references in the message field. Do **NOT** put internal `stock_id` values (like `--7w5ouKQ6mb6LoXqhyn7`) in the message field; they are opaque and unreadable to users. Express all position sizes as percentages (multiply decimal by 100) with up to 2 decimal places for precision (e.g., 0.5 → \"50%\", 1.0 → \"100%\", 0.755 → \"75.50%\", 1.5 → \"150%\"). Do not include decimal values in the message - only percentages.  \n    - For posts (action = 'post'):  \n      - **User-authored posts**: Simple confirmation of the post that was submitted (e.g., \"Post submitted to audience about...\")  \n      - **Agent-generated posts**: Confirm what type of analysis or content was posted (e.g., \"Posted technical analysis of your current positions to audience\", \"Posted Bitcoin technical analysis to audience\")  \n    - For none (action = 'none'): Answer user's trading-related questions or explain why no action was taken. **IMPORTANT**: Only show position information if the user explicitly asks about their positions or allocations. Do not include position details for general questions or unrelated input. When showing position information (if requested), always use the human-readable `symbol` and percentage format.\n\n### Tools\n\n#### Search Stocks\nHTTP request to AlphaInsider to search for stocks/cryptocurrencies and retrieve comprehensive information about matching securities.\n\n**When to use**: Call this tool for any security mentioned in the input **whose `stock_id` is not already available in the Positions input**. The returned `stock_id` is the authoritative identifier you MUST use in allocations. Also use it when you need to disambiguate a ticker or confirm a security exists on AlphaInsider.\n\n**CRITICAL - STOCK_ID USAGE**:  \nThe `stock_id` returned by this tool **IS** the value you put into the `stock_id` field in allocations. Use it verbatim. The internal `stock_id` is the single source of truth; the `symbol`, `stock`, and `exchange` fields are only for human-readable references (message field) and whitelist matching.\n\n**Common scenarios**:  \n- Any security mentioned in the input that is not already represented in Positions  \n- Unfamiliar company names (e.g., \"Palantir\", \"Coinbase\", \"ARM Holdings\")  \n- Ambiguous tickers that could be on multiple exchanges  \n- New or lesser-known stocks/cryptocurrencies  \n- When you need to verify a security exists on AlphaInsider before trading  \n\n**If a security cannot be found**: Do NOT include it in the allocations. If some or all requested securities are unavailable, continue with any available ones and list the unavailable securities in the `message` field.\n\n**Parameters**:  \n- `search` (required): The name, symbol, or stock of the security to look for (e.g., \"Tesla\", \"TSLA\", \"Bitcoin\", \"BTC\", \"Palantir\")  \n- `type` (optional): Either 'cryptocurrency' or 'stock'. Only fill this in if you know the type of security it is.  \n\n**Returns**: The tool returns an array of matching stock/cryptocurrency objects based on the input search query. Each object contains:  \n- **stock_id**: The canonical internal AlphaInsider identifier (opaque string, e.g. `--7w5ouKQ6mb6LoXqhyn7`). **Use this value verbatim as the `stock_id` field in allocations.** This is the single source of truth for order creation.  \n- **symbol**: The trading symbol (e.g., `TSLA`, `BTC-USD`). Use for the message field and for whitelist matching.  \n- **name**: Full company/asset name (e.g., \"Tesla, Inc. Common Stock\")  \n- **exchange**: The exchange where the security trades (e.g., `NASDAQ`, `NYSE`, `COINBASE`). Optional human-readable context. Do NOT concatenate into the `stock_id` field.  \n- **stock**: The asset ticker (same as or a base variant of `symbol`).  \n- **security**: Type of security (`stock` or `cryptocurrency`). Use this to enforce the no-mixing rule (Rule #8).  \n- **sector**: The industry sector (for stocks, e.g., \"Manufacturing\")  \n- **figi_composite**: Financial Instrument Global Identifier  \n- **provider**: The data provider AlphaInsider uses (e.g., \"polygon\")  \n- **peg**: Currency denomination (e.g., \"USD\")  \n- **stock_status**: Current status (e.g., \"active\")  \n- **Price data**:  \n  - **bid**: Current bid price  \n  - **ask**: Current ask price  \n  - **last**: Last traded price  \n- **slippage**: Slippage value  \n- **fee**: Fee value  \n- **links**: Object containing URLs to third-party resources:  \n  - **finviz**: Finviz stock page URL  \n  - **trading_view**: TradingView chart URL  \n  - **yahoo_finance**: Yahoo Finance quote URL  \n  - **google_finance**: Google Finance quote URL  \n\n**Important**: For the allocations output, use `stock_id` verbatim. For the message field, use the human-readable `symbol` (e.g. `TSLA`) — never the opaque `stock_id`. Price information (bid/ask/last) and other details can be referenced when providing context in the message field if relevant to the user's query.\n\n### Calculator\nBasic arithmetic calculator for performing calculations.\n\n**When to use**: Use this tool for **ALL arithmetic operations** rather than calculating in your head. This includes but is not limited to:  \n- Summing allocation `percent` values to verify the total does not exceed 2.0  \n- Calculating new position sizes (e.g., reducing a 1.0 position by 25% → `1.0 - (1.0 * 0.25)`)  \n- Splitting allocations equally or proportionally (e.g., splitting 2.0 across 3 assets → `2.0 / 3`)  \n- Converting decimals to percentages for the message field (e.g., `0.755 * 100`)  \n- Any other arithmetic needed to determine correct allocation values  \n\n**Example usage**:  \n- Summing allocations: `1.5 + 0.5` → `2.0` ✓ (valid, ≤ 2.0)  \n- Summing allocations: `1.0 + 0.75 + 0.5` → `2.25` ✗ (exceeds 2.0)  \n- Reducing a position by 25%: `1.0 - (1.0 * 0.25)` → `0.75`  \n- Equal split across 4 assets: `2.0 / 4` → `0.5` each  \n\n### Memory\nSimple memory module that stores conversation history from previous trading signals.\n\n**When to use**:  \n- Use this tool only when the current Input text is ambiguous and requires context from previous messages to make a decision.  \n- Use memory as **supplementary context only** to understand recent patterns, sentiment, or related signals.  \n- **DO NOT rely heavily on memory** - the current Input text is always the primary source for decision-making.  \n- Memory may help clarify ambiguous references (e.g., \"I'm adding more\" when previous message mentioned a stock).  \n- If memory contradicts current input, **prioritize the current input**.  \n- Treat memory as helpful background information, not as trading instructions.  \n\n**How it works**:  \n- The memory module automatically maintains conversation history.  \n- Reference it to clarify ambiguous inputs, but always prioritize the current Input text as the primary source for trading decisions.\n\n### Examples:\n\n(Note: In the examples below, `stock_id` values in both allocations and Positions input are shown as symbolic placeholders — e.g. `9ot8fZX7romhU2Q8kV97r` as TSLA, `64diisPJwIqt99jyjoIGT` as BTC-USD — to represent the opaque internal AlphaInsider identifier resolved at runtime via the Search Stocks tool or carried over from the Positions input. The `message` field always uses the human-readable `symbol`, never the opaque id. The `Whitelist:` line is an array of plain symbol strings.)\n\nExample 8 (User-Specified Percentages):\nInput text: Buy TSLA at 50% and NVDA at 25%\nContext: Direct trading signal\nWhitelist: [\"TSLA\", \"NVDA\", \"MSFT\"]\nPositions: []\n→ Output:\n`{\n  \"action\": \"trade\",\n  \"allocations\": [\n    {\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"action\": \"long\", \"percent\": 0.5},\n    {\"stock_id\": \"HYYQf3BI--ThQ-KbIYm-X\", \"action\": \"long\", \"percent\": 0.25}\n  ],\n  \"message\": \"Opened TSLA long at 50% and NVDA long at 25% as specified. Current positions: TSLA long 50%, NVDA long 25%.\"\n}`\n(Note: Neither TSLA nor NVDA is in Positions, so both are resolved via Search Stocks. Total = 0.75, under 2.0 — do NOT auto-fill to 2.0 when user specifies exact percentages.)\n\nExample 9 (User-Specified Rules - No Leverage):\nInput text: Go long BTC but no leverage, keep it at 100% max\nContext: Direct trading signal\nWhitelist: [\"BTC-USD\", \"ETH-USD\"]\nPositions: []\n→ Output:\n`{\n  \"action\": \"trade\",\n  \"allocations\": [\n    {\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"action\": \"long\", \"percent\": 1.0}\n  ],\n  \"message\": \"Opened BTC-USD long at 100% with no leverage as requested. Current positions: BTC-USD long 100%.\"\n}`\n(Note: BTC not in Positions → resolved via Search Stocks. User explicitly said no leverage and 100% max — respect that rule.)\n\nExample 10 (Post to Audience with URL):\nInput text: Bitcoin breaking key resistance at $45k! This could signal a major move. Check out the chart: https://tradingview.com/btc-analysis\nContext: Social media post\nWhitelist: [\"BTC-USD\"]\nPositions: [{\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"symbol\": \"BTC-USD\", \"stock\": \"BTC\", \"exchange\": \"COINBASE\", \"action\": \"long\", \"percent\": 1.0}]\n→ Output:\n`{\n  \"action\": \"post\",\n  \"allocations\": [],\n  \"post\": {\n    \"description\": \"Bitcoin breaking key resistance at $45k! This could signal a major move. Check out the chart: https://tradingview.com/btc-analysis\",\n    \"url\": \"https://tradingview.com/btc-analysis\"\n  },\n  \"message\": \"Post submitted to audience about Bitcoin breaking resistance.\"\n}`\n(Note: User is posting analysis to their audience, not making a trade; action = post.)\n\nExample 11 (Direct Q&A):\nInput text: What are my current positions?\nContext: Direct question to agent\nWhitelist: [\"TSLA\", \"MSFT\"]\nPositions: [{\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"symbol\": \"TSLA\", \"stock\": \"TSLA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"KUF7Djr52VEBjQ6sN1NTh\", \"symbol\": \"MSFT\", \"stock\": \"MSFT\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 0.5}]\n→ Output:\n`{\n  \"action\": \"none\",\n  \"allocations\": [],\n  \"message\": \"Your current positions are: TSLA long 100%, MSFT long 50%. Total allocation: 150%.\"\n}`\n(Note: Direct question; action = none. Message uses human-readable symbols, never the opaque stock_id.)\n\nExample 12 (Post without URL):\nInput text: Market looking shaky today. Stay cautious everyone!\nContext: Social media post\nWhitelist: []\nPositions: []\n→ Output:\n`{\n  \"action\": \"post\",\n  \"allocations\": [],\n  \"post\": {\n    \"description\": \"Market looking shaky today. Stay cautious everyone!\"\n  },\n  \"message\": \"Post submitted to audience about market caution.\"\n}`\n(Note: General market commentary to audience; action = post, no URL.)\n\nExample 13 (Off-topic Question):\nInput text: What's the weather like today?\nContext: Direct question to agent\nWhitelist: [\"TSLA\", \"MSFT\"]\nPositions: [{\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"symbol\": \"TSLA\", \"stock\": \"TSLA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}]\n→ Output:\n`{\n  \"action\": \"none\",\n  \"allocations\": [],\n  \"message\": \"I can only assist with trading signals, stock/cryptocurrency analysis, and position management questions.\"\n}`\n(Note: Off-topic question; agent declines.)\n\nExample 14 (Agent-Generated Post - Current Positions):\nInput text: Make a post about my current positions\nContext: Direct request to agent\nWhitelist: [\"TSLA\", \"MSFT\", \"NVDA\"]\nPositions: [{\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"symbol\": \"TSLA\", \"stock\": \"TSLA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"HYYQf3BI--ThQ-KbIYm-X\", \"symbol\": \"NVDA\", \"stock\": \"NVDA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 0.5}]\n→ Output:\n`{\n  \"action\": \"post\",\n  \"allocations\": [],\n  \"post\": {\n    \"description\": \"Current portfolio allocation: Long TSLA at 100% and NVDA at 50%. Maintaining tech-heavy exposure with focus on EV and AI semiconductor sectors. Total allocation: 150%.\"\n  },\n  \"message\": \"Posted technical analysis of your current positions to audience.\"\n}`\n(Note: Agent generates professional post content about user's positions; action = post.)\n\nExample 15 (Agent-Generated Post - Specific Stock Analysis):\nInput text: Post an update on Bitcoin with technical analysis\nContext: Direct request to agent\nWhitelist: [\"BTC-USD\", \"ETH-USD\"]\nPositions: [{\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"symbol\": \"BTC-USD\", \"stock\": \"BTC\", \"exchange\": \"COINBASE\", \"action\": \"long\", \"percent\": 2.0}]\n→ Output:\n`{\n  \"action\": \"post\",\n  \"allocations\": [],\n  \"post\": {\n    \"description\": \"Bitcoin technical update: Currently holding full position at 200% allocation. BTC showing strength above key support levels. Watching for continuation patterns as momentum indicators remain bullish. Risk managed with defined allocation strategy.\"\n  },\n  \"message\": \"Posted Bitcoin technical analysis to audience.\"\n}`\n(Note: Agent creates technical analysis post; action = post.)\n\nExample 16 (Agent-Generated Post - Market Commentary):\nInput text: Make a post saying the tech sector looks strong this week\nContext: Direct request to agent\nWhitelist: [\"TSLA\", \"MSFT\", \"NVDA\"]\nPositions: [{\"stock_id\": \"KUF7Djr52VEBjQ6sN1NTh\", \"symbol\": \"MSFT\", \"stock\": \"MSFT\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"HYYQf3BI--ThQ-KbIYm-X\", \"symbol\": \"NVDA\", \"stock\": \"NVDA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}]\n→ Output:\n`{\n  \"action\": \"post\",\n  \"allocations\": [],\n  \"post\": {\n    \"description\": \"Tech sector showing strong momentum this week. Positioned in MSFT and NVDA with full allocations. Both names demonstrating solid technical structure and relative strength. Watching for continued outperformance in the space.\"\n  },\n  \"message\": \"Posted tech sector analysis to audience.\"\n}`\n(Note: Agent generates market commentary based on user's instruction and current positions; action = post.)\n\nExample 17 (Adjust Mode - Adding new position, preserving existing):\nInput text: Buy TSLA at 50%\nContext: Direct trading signal\nWhitelist: [\"TSLA\", \"BTC-USD\", \"ETH-USD\"]\nPosition Mode: adjust\nPositions: [{\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"symbol\": \"BTC-USD\", \"stock\": \"BTC\", \"exchange\": \"COINBASE\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"v3lhjrwEhNuAOxPT29oxO\", \"symbol\": \"ETH-USD\", \"stock\": \"ETH\", \"exchange\": \"COINBASE\", \"action\": \"long\", \"percent\": 0.5}]\n→ Output:\n`{\n  \"action\": \"trade\",\n  \"allocations\": [\n    {\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"action\": \"long\", \"percent\": 0.5},\n    {\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"action\": \"long\", \"percent\": 1.0},\n    {\"stock_id\": \"v3lhjrwEhNuAOxPT29oxO\", \"action\": \"long\", \"percent\": 0.5}\n  ],\n  \"message\": \"Opened TSLA long at 50%. Existing positions preserved. Current positions: TSLA long 50%, BTC-USD long 100%, ETH-USD long 50%. Total allocation: 200%.\"\n}`\n(Note: Position Mode is adjust — BTC and ETH are carried forward using their original `stock_id` values. TSLA is not in Positions, so its `stock_id` is resolved via Search Stocks. Total = 2.0.)\n\nExample 18 (Adjust Mode - Closing one specific position, preserving others):\nInput text: Sell my ETH position\nContext: Direct trading signal\nWhitelist: [\"BTC-USD\", \"ETH-USD\"]\nPosition Mode: adjust\nPositions: [{\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"symbol\": \"BTC-USD\", \"stock\": \"BTC\", \"exchange\": \"COINBASE\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"v3lhjrwEhNuAOxPT29oxO\", \"symbol\": \"ETH-USD\", \"stock\": \"ETH\", \"exchange\": \"COINBASE\", \"action\": \"long\", \"percent\": 0.5}]\n→ Output:\n`{\n  \"action\": \"trade\",\n  \"allocations\": [\n    {\"stock_id\": \"64diisPJwIqt99jyjoIGT\", \"action\": \"long\", \"percent\": 1.0}\n  ],\n  \"message\": \"Closed ETH-USD position. BTC-USD position preserved. Current positions: BTC-USD long 100%.\"\n}`\n(Note: Only ETH is removed because the input explicitly says to sell ETH (matched via the position's `stock` or `symbol` field). BTC is carried forward unchanged using its original internal `stock_id`.)\n\nExample 19 (Replace Mode - Full portfolio replacement):\nInput text: Going all in on NVDA\nContext: Direct trading signal\nWhitelist: [\"TSLA\", \"NVDA\", \"MSFT\"]\nPosition Mode: replace\nPositions: [{\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"symbol\": \"TSLA\", \"stock\": \"TSLA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"KUF7Djr52VEBjQ6sN1NTh\", \"symbol\": \"MSFT\", \"stock\": \"MSFT\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 0.5}]\n→ Output:\n`{\n  \"action\": \"trade\",\n  \"allocations\": [\n    {\"stock_id\": \"HYYQf3BI--ThQ-KbIYm-X\", \"action\": \"long\", \"percent\": 2.0}\n  ],\n  \"message\": \"Closed TSLA and MSFT positions. Opened NVDA long at 200%. Current positions: NVDA long 200%.\"\n}`\n(Note: Position Mode is replace — existing TSLA and MSFT are NOT carried forward. NVDA is not in Positions, so its `stock_id` is resolved via Search Stocks.)\n\nExample 20 (Adjust Mode - Existing positions reduced to fit new trade):\nInput text: Buy MSFT at 100%\nContext: Direct trading signal\nWhitelist: [\"TSLA\", \"MSFT\", \"NVDA\"]\nPosition Mode: adjust\nPositions: [{\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"symbol\": \"TSLA\", \"stock\": \"TSLA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 1.0}, {\"stock_id\": \"HYYQf3BI--ThQ-KbIYm-X\", \"symbol\": \"NVDA\", \"stock\": \"NVDA\", \"exchange\": \"NASDAQ\", \"action\": \"long\", \"percent\": 0.75}]\n→ Output:\n`{\n  \"action\": \"trade\",\n  \"allocations\": [\n    {\"stock_id\": \"KUF7Djr52VEBjQ6sN1NTh\", \"action\": \"long\", \"percent\": 1.0},\n    {\"stock_id\": \"9ot8fZX7romhU2Q8kV97r\", \"action\": \"long\", \"percent\": 0.5714},\n    {\"stock_id\": \"HYYQf3BI--ThQ-KbIYm-X\", \"action\": \"long\", \"percent\": 0.4286}\n  ],\n  \"message\": \"Opened MSFT long at 100% as requested. Existing positions proportionally reduced to stay within 200% total allocation limit: TSLA reduced from 100% to 57.14%, NVDA reduced from 75% to 42.86%. Current positions: MSFT long 100%, TSLA long 57.14%, NVDA long 42.86%. Total allocation: 200%.\"\n}`\n(Note: Position Mode is adjust — the user's new trade takes highest priority at the full 100%. Existing TSLA (1.0) + NVDA (0.75) = 1.75 are proportionally scaled down to fit the remaining 1.0 of capacity. MSFT's `stock_id` is resolved via Search Stocks; TSLA and NVDA keep their original internal `stock_id` values.)\n\n### Final Output Instructions:\n- Output **ONLY** valid JSON. No explanations, no markdown, no extra text.  \n- Use this exact structure:  \n  `{  \n    \"action\": \"trade\" or \"post\" or \"none\",  \n    \"allocations\": [  \n      {  \n        \"stock_id\": \"<internal AlphaInsider stock_id from Search Stocks or Positions input>\",  \n        \"action\": \"long\" or \"short\" or \"close\",  \n        \"percent\": 0.75  \n      }  \n    ],  \n    \"post\": {  \n      \"description\": \"exact user message to audience\",  \n      \"url\": \"optional main URL\"  \n    },  \n    \"message\": \"direct message back to user about what was done\"  \n  }`  \n- Set action: 'trade' when there is a clear trading signal (populate allocations; allocations may be empty for general close orders)  \n- Set action: 'post' when user is posting to their audience (populate post object with description and optional url)  \n- Set action: 'none' when no clear signal, user asking questions, or input is unrelated (provide helpful response in message)  \n- The `post` object should only be included when action = 'post'  \n- The `message` field is always required and provides context about what action was taken  \n- **FINAL REMINDER ON STOCK_ID**: In any trade action, every `stock_id` **must** be the internal AlphaInsider identifier — sourced from the Positions input when the security is already held, or from the Search Stocks tool otherwise. **Never** use `SYMBOL` strings (e.g. `TSLA`, `BTC-USD`) as the `stock_id`. Human-readable symbols belong in the `message` field only. This is non-negotiable.\n",
          "maxIterations": 10
        }
      },
      "id": "e928b305-d42b-4064-8ee5-8223ae61c359",
      "name": "Parse Stock Allocations",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        5392,
        2576
      ],
      "retryOnFail": false,
      "maxTries": 2
    },
    {
      "parameters": {
        "toolDescription": "Makes an HTTP request to searchStocks for finding the information for a stock traded on AlphaInsider. The returned values are a listing of potential stocks the search item could be.",
        "method": "POST",
        "url": "https://alphainsider.com/api/searchStocks",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "search",
              "value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('parameters0_Value', `(REQUIRED String) The name or symbol of the stock to look for (e.g., \"Tesla\", \"TSLA\", \"Bitcoin\", \"BTC\")`, 'string') }}"
            },
            {
              "name": "type",
              "value": "stock"
            },
            {
              "name": "limit",
              "value": "3"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.3,
      "position": [
        5552,
        2816
      ],
      "id": "6e96a537-abff-4260-ae18-ada91fd6e9c5",
      "name": "Search Stocks"
    },
    {
      "parameters": {},
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [
        5696,
        2816
      ],
      "id": "5f284425-5721-4a8f-8949-09c3db39efe1",
      "name": "Calculator"
    },
    {
      "parameters": {
        "jsCode": "// Get http response input from AI\nlet allocations = $('Parse Positions').first().json.allocations;\n\n//parse percents\nlet results = allocations\n.map(allocation => {\n  return {\n    json: {\n      ...allocation,\n      percent: Math.trunc(parseFloat(allocation.percent) * 10000) / 10000\n    }\n  };\n})\n//only get percents >= 0.01%\n.filter(allocation => parseFloat(allocation.json.percent) >= 0.0001)\n//sort by percent (highest to lowest)\n.sort((a, b) => parseFloat(b.json.percent) - parseFloat(a.json.percent))\n//take only the top 100\n.slice(0, 100);\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6128,
        2416
      ],
      "id": "4f3d8d2c-be8a-41db-976a-46ee0a60f384",
      "name": "Parse Orders Allocations",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://alphainsider.com/api/newPost",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "description",
              "value": "={{ $json.output.post.description }}"
            },
            {
              "name": "url",
              "value": "={{ $json.output.post.url }}"
            },
            {
              "name": "strategy_id",
              "value": "={{ $('Global Settings').item.json.strategy_id }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        6128,
        2576
      ],
      "id": "18e0d7c2-5b15-47ca-a434-ff82379dd820",
      "name": "Create Post",
      "executeOnce": true,
      "credentials": {
        "httpBearerAuth": {
          "id": "8bECTX76NIRrpJ6Z",
          "name": "CongressTracker"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.report_type }}",
                    "rightValue": "P",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "b9696b5f-0859-4c1b-97ab-f4ccb66a5541"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "PTR"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "f9abcc71-6ae6-461e-a5a3-c15807d1b9f6",
                    "leftValue": "={{ $json.report_type }}",
                    "rightValue": "O",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "FD"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        3552,
        2592
      ],
      "id": "a66ebbbb-9929-4521-8a58-ae7af53af3b7",
      "name": "Report Switch"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "trade",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "80df08f7-ec9e-49a4-bf83-62c0617a0db3"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Trade Action"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "9d580421-c579-452a-b462-a0740922eb0e",
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "post",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Post Action"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "56ce5453-15ea-4a5f-9f11-afd31b4ed0c6",
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "none",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "No Action"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        5856,
        2560
      ],
      "id": "f6d0c2bf-5869-451e-9077-445e88fcfacd",
      "name": "Strategy Action Switch"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let holdings = $json.holdings;\nlet reportType = $json.report_type;\n\ntry {\n  holdings = JSON.parse(holdings);\n} catch(e) {\n  console.log(\"Failed to parse holdings\", e);\n  holdings = [];\n}\n\n// Generate natural language text describing exactly what the next trades should be\nlet naturalText = `Place these trades based on the latest congressional filings:\\n\\n`;\n\nif (holdings && holdings.length > 0) {\n  holdings.forEach(holding => {\n    const symbol = holding.Symbol;\n    const action = holding.Action || 'Buy';\n    //set to percent and maxout 2x leverage\n    const percent = (parseFloat(holding.Percent) * 100 * 2).toFixed(3);\n    \n    if(['ST', 'EF'].includes(holding.AssetType) && parseFloat(percent) >= 0.01) naturalText += `• ${action} ${symbol} — ${percent}%\\n`;\n  });\n} else {\n  naturalText += \"No new trade actions at this time.\\n\";\n}\n\n//add rules for the AI bot\nnaturalText += \"\\n\\n**CRITICAL**: NO SHORTING ALLOWED.\";\nnaturalText += `\\n**IMPORTANT:**\n- If any symbol from the list cannot be located or is unavailable, skip **only** that specific symbol and continue executing all remaining trades in the list.`;\n\nreturn { json: {...$json, input: naturalText} };"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3344,
        2592
      ],
      "id": "2977d6ae-c256-4703-8397-ee55cf14f980",
      "name": "Parse Congress Holdings"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT * FROM congress_reports\nWHERE congress_member_id = '{{ $('Congress Trigger').item.json.congress_member_id }}'\n  AND report_type = 'P'\n  AND filed_at >= make_date({{ new Date($('Congress Trigger').item.json.filed_at).getFullYear() }}, 1, 1)\nORDER BY filed_at ASC;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1568,
        2592
      ],
      "id": "86346039-9daa-47c4-b4ca-d2edd19df6a0",
      "name": "Select PTRs",
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "id": "1tho5Ld1N34iwkFW",
          "name": "N8N Postgres"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nlet reports = [];\nfor (const item of $input.all()) {\n  if(item.json.holdings?.length > 0) reports.push(item.json.holdings);\n}\n\nreturn [{json: {reports: JSON.stringify(reports)}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1744,
        2592
      ],
      "id": "8419204e-d424-42c4-b3a1-a44c3c1cf1c7",
      "name": "Combine PTR Holdings"
    },
    {
      "parameters": {
        "jsCode": "return [{json: JSON.parse($input.first().json.output)}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5680,
        2576
      ],
      "id": "c2595508-f014-43a1-a559-a1cd205736b1",
      "name": "Parse Positions"
    },
    {
      "parameters": {
        "jsCode": "const lookback = 3;\n\nlet result = [];\nfor(let i = lookback-1; i >= 0; i--) result.push({json: {year: new Date().getFullYear() - i}});\n\nreturn result;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1328,
        1200
      ],
      "id": "36ace657-d11b-43bb-856c-afc80dac0711",
      "name": "Lookback Years"
    },
    {
      "parameters": {
        "jsCode": "let results = [];\nfor(let item of $input.all()) {\n  try {\n    let member = JSON.parse(item.json.output);\n    results.push(member);\n  } catch(e) {}\n}\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2912,
        1376
      ],
      "id": "00e717e2-00e9-435c-84c1-9b503281cb1e",
      "name": "Parse Members"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ JSON.stringify($json) }}",
        "options": {
          "systemMessage": "You are a congressional member metadata extractor. Your job is to identify the correct US Congress or House member based on their name and return their party, state, and district.\n\n## Input\n\nThe user provides a JSON object with the following fields:\n\n```json\n{\n  \"first\": \"Alma Shealey\",\n  \"last\": \"Adams\",\n  \"doc_id\": \"10066961\",\n  \"report_type\": \"O\",\n  \"filed_at\": \"4/29/2025\",\n  \"year\": 2024\n}\n```\n\n**Important:** There is NO document context or PDF text provided. You must use your knowledge or search the internet to find information about this person.\n\n## Task\n\nUsing the **first** and **last** name from the input, identify the correct US Congress/House member and return:\n- Their political party\n- The state they represent\n- Their congressional district (if applicable)\n\n**Name matching is critical** - make sure you have the correct person. There may be multiple people with similar names.\n\n## Rules\n\nYou must identify the correct person using the provided first and last name. Use your knowledge or search the internet as needed.\n\n1. **Party**:\n   - `D` for Democrat\n   - `R` for Republican\n   - `I` for Independent\n\n2. **State**:\n   - Return the 2-letter US state code (e.g. `NC`, `CA`, `FL`, `NY`)\n\n3. **District**:\n   - For House members: return the district number as an integer (e.g. `12` for NC-12)\n   - For Senators: return `null`\n   - If district cannot be determined: return `null`\n\n**Priority**: Use the most current and accurate information available. If you're unsure about the exact person, prioritize the most prominent/relevant member with that name in Congress.\n\n## Output Requirements\n\n- Output **ONLY** valid JSON. No explanations, no markdown, no extra text.\n- The JSON must contain **all** the original input fields PLUS the three new metadata fields.\n- Required keys: `first`, `last`, `doc_id`, `report_type`, `filed_at`, `year`, `party`, `state`, `district`\n- `party` must be a single uppercase letter (`D`, `R`, or `I`).\n- `state` must be a 2-letter uppercase US state code.\n- `district` must be either a number (as integer) or `null`.\n\n### Example Output (House Member):\n\n```json\n{\n  \"first\": \"Alma Shealey\",\n  \"last\": \"Adams\",\n  \"doc_id\": \"10066961\",\n  \"report_type\": \"O\",\n  \"filed_at\": \"4/29/2025\",\n  \"year\": 2024,\n  \"party\": \"D\",\n  \"state\": \"NC\",\n  \"district\": 12\n}\n```\n\n### Example Output (Senator):\n\n```json\n{\n  \"first\": \"John\",\n  \"last\": \"Doe\",\n  \"doc_id\": \"12345678\",\n  \"report_type\": \"O\",\n  \"filed_at\": \"3/15/2025\",\n  \"year\": 2024,\n  \"party\": \"D\",\n  \"state\": \"CA\",\n  \"district\": null\n}\n```\n\n**Critical**: Only output the JSON. Do not include any other text, reasoning, or formatting. The output must preserve all original input fields exactly as provided."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2640,
        1376
      ],
      "id": "99299016-8302-4240-b07b-b43a13a307fe",
      "name": "Lookup Agent"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- 1. Update existing record if name matches\nUPDATE congress_members\nSET\n  location = $1,\n  updated_at = NOW()\nWHERE first = $2\n  AND last = $3;\n\n-- 2. Insert new record only if no name match exists\nINSERT INTO congress_members (\n  congress_member_id, \n  strategy_id, \n  first, \n  last, \n  location\n)\nSELECT\n  $4,-- random ID\n  $5,-- strategy_id\n  $2,-- first (reused)\n  $3,-- last (reused)\n  $1 -- location (reused)\nWHERE NOT EXISTS (\n  SELECT 1 \n  FROM congress_members\n  WHERE first = $2 \n    AND last = $3\n)\nRETURNING *;",
        "options": {
          "queryReplacement": "={{\n  [\n    // $1 = location\n    $('Parse Members').item.json.party + '-' + \n      $('Parse Members').item.json.state + \n      ($('Parse Members').item.json.district ? '-' + $('Parse Members').item.json.district : ''),\n    // $2 = first\n    $('Parse Members').item.json.first,\n    // $3 = last\n    $('Parse Members').item.json.last,\n    // $4 = random congress_member_id\n    Math.random().toString(36).slice(2, 14),\n    // $5 = strategy_id\n    $('New Strategy').item.json.response.strategy_id\n  ]\n}}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        3264,
        1376
      ],
      "id": "8c2108cd-dbcb-4feb-8547-5a17b22e9365",
      "name": "Upsert Members",
      "credentials": {
        "postgres": {
          "id": "1tho5Ld1N34iwkFW",
          "name": "N8N Postgres"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://alphainsider.com/api/newStrategy",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "type",
              "value": "stock"
            },
            {
              "name": "name",
              "value": "=({{ $json.party }}-{{ $json.state }}{{ ($json.district ? '-'+$json.district : '') }}) {{ $json.first }} {{ $json.last }} Portfolio"
            },
            {
              "name": "input_value",
              "value": "100000"
            },
            {
              "name": "description",
              "value": "CongressTracker is an n8n workflow that automatically turns official US Congressional stock trades into actionable AlphaInsider trading signals. It monitors public financial disclosures filed by members of Congress, extracts and structures their stock transactions using AI (primarily Grok), reconciles them against baseline holdings, and feeds clean buy/sell/close signals into AlphaInsiders PositionManager engine for automated trading, posting, or analysis.\n\n### How it works\n- **Daily schedule (10:00 AM EST)**: Checks the Clerk of the House website for new Financial Disclosure (FD) and Periodic Transaction Report (PTR) PDFs.\n- **AI parsing**: Grok (with OpenAI Vision fallback) reads the PDFs, extracts ticker symbols, transaction types (purchase, sale, partial sale, exchange), dates, and value ranges.\n- **Reconciliation**: Combines the annual FD baseline portfolio with chronological PTR trades to calculate current positions, apply netting rules, and generate standardized signals (buy/sell/close) with midpoint values.\n- **Enrichment & execution**: Adds member metadata (party, state, district), checks exchange hours, respects leverage limits and optional whitelists, then either executes the trade on AlphaInsider, creates an audience post, or logs it for tracking.\n- **Storage**: All processed filings are saved in a PostgreSQL database for historical analysis and backtesting. A weekly stock-data refresh keeps the local reference table in sync with AlphaInsider.\n\nThe full public n8n template is available here for anyone to import and use:  \nhttps://alphainsider-n8n-templates.s3.us-east-2.amazonaws.com/CongressTracker.json"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3088,
        1376
      ],
      "id": "fcb026af-f529-4ab6-ac74-9c2de8474660",
      "name": "New Strategy",
      "credentials": {
        "httpBearerAuth": {
          "id": "8bECTX76NIRrpJ6Z",
          "name": "CongressTracker"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "let docs = $('Find Latest Docs').all().map(doc => doc.json);\nlet results = [];\nfor (const item of $input.all()) {\n  let doc = docs.find((value) => {\n    return (item.json.first.toLowerCase() === value.first.toLowerCase() &&\n      item.json.last.toLowerCase() === value.last.toLowerCase());\n  });\n  results.push({\n    json: {\n      ...doc,\n      ...item.json\n    }\n  });\n}\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3456,
        1376
      ],
      "id": "07e2770c-0206-4690-ba7d-e26711cbc7e5",
      "name": "Combine Member Docs"
    },
    {
      "parameters": {
        "url": "https://alphainsider.com/api/getPositions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "strategy_id",
              "value": "={{ $('Global Settings').item.json.strategy_id }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4928,
        2576
      ],
      "id": "50d18177-1033-4013-bbb9-89eac6c7a628",
      "name": "Get Positions",
      "credentials": {
        "httpBearerAuth": {
          "id": "8bECTX76NIRrpJ6Z",
          "name": "CongressTracker"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://alphainsider.com/api/newOrderAllocations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"strategy_id\": $('Global Settings').item.json.strategy_id, \"allocations\": $json.allocations } }}",
        "options": {}
      },
      "id": "86f8a344-69bb-4b73-b8e3-b0eec83dd824",
      "name": "Create Orders",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        6640,
        2416
      ],
      "executeOnce": true,
      "credentials": {
        "httpBearerAuth": {
          "id": "8bECTX76NIRrpJ6Z",
          "name": "CongressTracker"
        }
      }
    },
    {
      "parameters": {
        "batchSize": 5,
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        2480,
        1296
      ],
      "id": "ea801384-dbc9-4f2b-bec5-65dc8e3f5bad",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "fdbzwBwwCf9SVTCd",
          "mode": "list",
          "cachedResultUrl": "/workflow/fdbzwBwwCf9SVTCd",
          "cachedResultName": "Congress Tracker"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "mode": "each",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        3648,
        1376
      ],
      "id": "09b91bf5-039a-47d1-b1a6-61d19a310dc9",
      "name": "Call Congress Trigger - New"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "fdbzwBwwCf9SVTCd",
          "mode": "list",
          "cachedResultUrl": "/workflow/fdbzwBwwCf9SVTCd",
          "cachedResultName": "Congress Tracker"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "mode": "each",
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        2624,
        1088
      ],
      "id": "9576f0c4-bc6d-42a9-8832-35cf8037985d",
      "name": "Call Congress Trigger - Update",
      "alwaysOutputData": false,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $('Global Settings').item.json.session_id }}",
        "contextWindowLength": 10
      },
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.3,
      "position": [
        5408,
        2816
      ],
      "id": "45d1287a-1738-4e3f-bca9-f4dfe7d01eac",
      "name": "Simple Memory"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.strategy_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "id": "e63cefdb-2207-4c76-8f45-610d350b2ab7"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Has Strategy"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "53b6b12b-7b70-4997-a3df-c3a44a9f766c",
                    "leftValue": "={{ $json.report_type }}",
                    "rightValue": "O",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "FD Report"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        2256,
        1200
      ],
      "id": "7b7f1b08-4202-48c3-80fd-8d4cfe54ec34",
      "name": "Has Strategy"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "965b6d95-f191-4dfd-b6d8-d320c52ab1ef",
              "leftValue": "={{ $json.response.stock }}",
              "rightValue": "open",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1008,
        1200
      ],
      "id": "3fb9efc0-7d69-46d4-b763-1d7ee919388c",
      "name": "Check If Exchange Is Open"
    },
    {
      "parameters": {
        "url": "https://alphainsider.com/api/getExchangeStatus",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        848,
        1200
      ],
      "id": "a0aad29d-2d0b-4344-8a71-4a6b85c38c15",
      "name": "Exchange Status"
    },
    {
      "parameters": {
        "operation": "deleteTable",
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "congress_reports",
          "mode": "list",
          "cachedResultName": "congress_reports"
        },
        "deleteCommand": "delete",
        "where": {
          "values": [
            {
              "column": "doc_id",
              "value": "={{ $json.doc_id }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2800,
        1104
      ],
      "id": "a7cfa84a-63c7-418e-b286-9898f3d97683",
      "name": "Delete Report",
      "credentials": {
        "postgres": {
          "id": "1tho5Ld1N34iwkFW",
          "name": "N8N Postgres"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        2480,
        976
      ],
      "id": "65b8e608-2490-4206-9ab8-f16611c5b69e",
      "name": "Loop Over Reports"
    },
    {
      "parameters": {
        "resource": "table",
        "operation": "create",
        "tableName": "stocks",
        "columns": {
          "column": [
            {
              "name": "stock_id"
            },
            {
              "name": "security"
            },
            {
              "name": "name"
            },
            {
              "name": "stock"
            },
            {
              "name": "symbol"
            },
            {
              "name": "exchange"
            },
            {
              "name": "created_at",
              "type": "date"
            }
          ]
        },
        "options": {
          "createIfNotExists": false
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1568,
        688
      ],
      "id": "cd758ba1-39ae-429e-b000-880f14d0681d",
      "name": "Create stocks table"
    },
    {
      "parameters": {
        "url": "https://alphainsider.com/api/getAllStocks",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1728,
        688
      ],
      "id": "9bf632ed-731b-49c8-be18-d8548953881c",
      "name": "Get All Stocks"
    },
    {
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "value": "={{ $('Create stocks table').item.json.id }}",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2080,
        688
      ],
      "id": "90bae5c5-155c-4082-909d-b3e012469827",
      "name": "Insert row"
    },
    {
      "parameters": {
        "jsCode": "let stocks = $input.first().json.response;\nlet results = stocks.map(stock => {\n  return {\n    json: {\n      stock_id: stock.stock_id,\n      security: stock.security,\n      stock: stock.stock,\n      symbol: stock.symbol,\n      exchange: stock.exchange,\n      name: stock.name,\n      created_at: new Date().toISOString()\n    }\n  };\n});\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1904,
        688
      ],
      "id": "8bdaa2ec-9997-4811-9bfb-749bfe0ea6e4",
      "name": "Parse Stocks"
    },
    {
      "parameters": {
        "operation": "rowExists",
        "dataTableId": {
          "__rl": true,
          "value": "stocks",
          "mode": "name"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "stock_id",
              "keyValue": "={{ $json.stock_id }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        6304,
        2416
      ],
      "id": "5ddd9871-68b5-421b-820e-fd4aed351733",
      "name": "Verify Stocks",
      "alwaysOutputData": true,
      "executeOnce": false
    },
    {
      "parameters": {
        "jsCode": "let allocations = $input.all()\n  .map(data => data.json)\n  .filter(data => Object.keys(data).length > 0);\nreturn [{json: {allocations}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6464,
        2416
      ],
      "id": "d71f720d-00ce-476d-8628-ebf830b2120f",
      "name": "Parse Allocations",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "resource": "table",
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        832,
        688
      ],
      "id": "ce215d36-0dce-47ff-9d13-fd6f974b2e81",
      "name": "List data tables"
    },
    {
      "parameters": {
        "resource": "table",
        "operation": "delete",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $json.tables[0].id }}",
          "mode": "id"
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1360,
        544
      ],
      "id": "24034bca-c9fe-4496-a0b6-7abc155ddc16",
      "name": "Delete stocks table"
    },
    {
      "parameters": {
        "jsCode": "let results = $input.all().map(item => ({id: item.json.id, name: item.json.name})).filter(item => item.name === 'stocks');\nreturn [{json: {tables: results}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        992,
        688
      ],
      "id": "b06c9abd-de25-4fcb-bade-92051d43831b",
      "name": "Parse Tables"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "a9c5edd3-6721-49d4-a28b-2acf7ec5e9c7",
              "leftValue": "={{ $json.tables }}",
              "rightValue": 0,
              "operator": {
                "type": "array",
                "operation": "lengthGt",
                "rightType": "number"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1152,
        688
      ],
      "id": "b49511ec-bcc7-40f5-be2f-b9505770c2fd",
      "name": "If stocks table exists"
    },
    {
      "parameters": {
        "content": "# Congress Tracker\n\n**n8n workflow that turns US Congressional stock trades into AlphaInsider trading signals.**\n\nThis project monitors official congressional financial disclosures, extracts trades from Periodic Transaction Reports (PTRs), and sends structured trading signals to **AlphaInsider** through the PositionManager engine.\n\n## Public Template\n\nImport the public n8n template from:\n\nhttps://alphainsider-n8n-templates.s3.us-east-2.amazonaws.com/CongressTracker.json\n\n## What It Does\n\nThe Congress Tracker:\n\n- **Monitors** new congressional financial disclosures from the Clerk of the House website\n- **Downloads** and parses PDF filings (FD and PTR)\n- **Extracts** structured transaction data using AI (Grok)\n- **Reconciles** baseline holdings from annual FD reports with subsequent PTR trades\n- **Generates** clean trading signals (`buy`/`sell`/`close`)\n- **Sends** signals to **AlphaInsider** via the PositionManager\n- **Enriches** data with member metadata (party, state, district)\n\n## Prerequisites\n\n- **n8n instance** (self-hosted or cloud)\n- **AlphaInsider account** with API access - [Sign up](https://alphainsider.com)\n- **xAI API key** (Grok or equivalent)\n- **OpenAI API key** (for Vision fallback)\n- **PostgreSQL** database (for storing processed filings)\n\n## Core Components\n\n### 1. FD Agent (`FD_AGENT.md`)\nExtracts baseline portfolio holdings from annual Financial Disclosure reports (Schedule A only).\n\n- Parses assets with ticker symbols ([ST], [EF], [MF] only)\n- Calculates midpoints from disclosure ranges\n- Computes cash position from non-stock assets\n- Produces standardized holdings JSON\n\n### 2. PTR Agent (`PTR_AGENT.md`)\nExtracts individual transactions from Periodic Transaction Reports.\n\n- Sophisticated parsing of transaction tables\n- Handles `P` (Purchase), `S` (Sale), `S (partial)`, `E` (Exchange)\n- Aggregates multiple lines per symbol+asset type\n- Applies intelligent netting rules for buys vs partial sells\n- Treats full `S` sales as position `close`\n\n### 3. Lookup Agent (`LOOKUP_AGENT.md`)\nEnriches member records with political metadata.\n\n- Identifies correct congress member from name\n- Returns party (`D`/`R`/`I`), state code, and district number\n\n### 4. Reconciliation Engine\nCombines FD baseline + chronological PTR transactions to compute current positions:\n- Applies buys, sells, and closes in date order\n- Maintains running cash balance\n- Remaps final midpoints to standard disclosure brackets\n- Outputs consistent JSON schema for downstream use\n\n## Data Sources\n\n- **Official Source**: https://disclosures-clerk.house.gov\n- **FD Index**: `https://disclosures-clerk.house.gov/public_disc/financial-pdfs/{{year}}FD.zip`\n- **PTR PDFs**: `https://disclosures-clerk.house.gov/public_disc/ptr-pdfs/{{year}}/{{doc_id}}.pdf`\n- **FD PDFs**: `https://disclosures-clerk.house.gov/public_disc/financial-pdfs/{{year}}/{{doc_id}}.pdf`\n\n## How It Works\n\n### Stock Data Refresh (Weekly Schedule)\n\nA scheduled trigger runs **every Sunday at 12:00 AM EST** to keep the local stocks data table in sync with AlphaInsider:\n\n1. **Check** if the `stocks` data table already exists\n2. **Delete** the table if it exists (ensures a clean refresh)\n3. **Create** a new `stocks` data table\n4. **Fetch** all available stocks from the AlphaInsider API (`GET https://alphainsider.com/api/getAllStocks`)\n5. **Parse & Insert** each stock into the table with:\n   - `stock_id` — the opaque AlphaInsider identifier (e.g., `--7w5ouKQ6mb6LoXqhyn7`), used verbatim when creating orders\n   - `security` — the security type\n   - `stock` / `symbol` — the trading ticker (e.g., `AAPL`, `BTC-USD`)\n   - `exchange` — the exchange where the security trades (e.g., `NASDAQ`, `COINBASE`)\n   - `name` — the human-readable stock name\n\nThis ensures the workflow always has an up-to-date reference of tradeable stocks when processing congressional filings.\n\n### Main Flow (Daily Schedule)\n\nA scheduled trigger runs **every day at 10:00 AM EST** to check for new congressional filings and process them.\n\n**Key Steps:**\n\n1. **Discovery**: Downloads and parses the official FD index ZIP to find new filings\n2. **Extraction**: AI agents (Grok) parse PDF tables; if text extraction fails, OpenAI Vision is used as a fallback to read the PDF as an image\n3. **Reconciliation**: Combines annual baseline holdings (FD) with individual transactions (PTR) in chronological order\n4. **Signal Generation**: Converts congressional trades into `buy`/`sell`/`close` signals with calculated midpoints\n5. **Position Analysis**: The Position Manager agent analyzes signals against current portfolio, respecting leverage limits (0–200%) and optional whitelist constraints\n6. **Execution**: Routes the result — executes trades on AlphaInsider, creates an audience post, or takes no action — after verifying the exchange is open\n\n## Output Format\n\nBoth FD and reconciled PTR flows output arrays of objects with this structure:\n\n```json\n{\n  \"Symbol\": \"AAPL\",\n  \"AssetType\": \"ST\",\n  \"ValueRange\": \"$15,001 - $50,000\",\n  \"Midpoint\": 32500,\n  \"DocID\": \"10066169\",\n  \"FilingDate\": \"5/15/2025\",\n  \"FilingType\": \"O\",\n  \"Year\": 2024\n}\n```\n\nPTR flows additionally include `Action` (`buy` | `sell` | `close`) and `TransactionDate`.\n\n## Integration with PositionManager\n\nThis workflow feeds congressional trading activity directly into the **PositionManager** (see `../PositionManager/` for details).\n\n**Position Manager Agent:**\n- Uses Grok with a **Search Stocks** tool to look up unfamiliar securities on AlphaInsider\n- Supports both **stocks** and **cryptocurrencies**\n- Enforces allocation limits of **0–2.0** (0–200% leverage)\n- Respects an optional **whitelist** of allowed symbols\n- Classifies each signal into one of three action types:\n  - **trade** — execute buy/sell/close orders on AlphaInsider\n  - **post** — publish an audience post about the signal (no trade)\n  - **none** — informational only, no action taken\n- Before executing trades, checks whether the relevant **exchange is currently open**\n\n**Signal Flow:**\n- Extracted congressional trades are converted to the same format used by Telegram/Discord bots\n- The PositionManager analyzes the signal with current portfolio context\n- Executes trades on AlphaInsider according to the same rules (leverage limits, whitelist support, etc.)\n- Maintains consistent position management across all input sources\n\n**Use Cases:**\n- Mirror politician trades automatically\n- Build strategies based on congressional buying/selling patterns\n- Track \"smart money\" moves from Congress members\n\nThe workflow stores all processed filings in PostgreSQL for historical analysis and backtesting.\n\n## Setup\n\n### Global Settings\n\nThe **Global Settings** node in the workflow requires:\n\n- **`strategy_id`** (required) — Your AlphaInsider strategy ID (found in the strategy URL)\n- **`whitelist`** (optional) — An array of allowed symbols in `SYMBOL:EXCHANGE` format (e.g., `[\"AAPL\", \"BTC-USD\"]`). Leave empty to allow all securities.\n\n### Credentials\n\nThe following credentials must be configured in n8n:\n\n| Credential | Used By | Purpose |\n|---|---|---|\n| **AlphaInsider API token** | Get Strategy Value, Get Positions, Create Orders, Create Post, New Strategy | Authenticate with AlphaInsider API |\n| **xAI API key** | Grok Model nodes | Power AI agents (Grok) |\n| **OpenAI API key** | Vision Fallback nodes | Vision fallback for PDF image extraction |\n| **PostgreSQL connection** | Get Members, Upsert Members, Insert Reports, Select PTRs, Update Congress Values | Store and query filing data |\n\n### Database\n\nThe workflow requires two PostgreSQL tables to store congress member data and their filing reports. Run the following SQL against your database to create them:\n\n```sql\n-- congress_members\nCREATE TABLE congress_members (\n  congress_member_id VARCHAR(100) NOT NULL PRIMARY KEY UNIQUE,\n  strategy_id VARCHAR(100),\n  first VARCHAR(100) NOT NULL,\n  last VARCHAR(100) NOT NULL,\n  location VARCHAR(100) NOT NULL,\n  multiplier DECIMAL(30, 15) NOT NULL DEFAULT 0,\n  updated_at TIMESTAMP NOT NULL DEFAULT now(),\n  created_at TIMESTAMP NOT NULL DEFAULT now()\n);\n\n-- congress_reports\nCREATE TABLE congress_reports (\n  congress_report_id VARCHAR(100) NOT NULL PRIMARY KEY UNIQUE,\n  congress_member_id VARCHAR(100) NOT NULL REFERENCES congress_members(congress_member_id) ON DELETE CASCADE ON UPDATE CASCADE,\n  doc_id VARCHAR(100) NOT NULL,\n  report_type VARCHAR(255) NOT NULL,\n  holdings JSONB NOT NULL DEFAULT '[]',\n  filed_at TIMESTAMP NOT NULL DEFAULT now(),\n  updated_at TIMESTAMP NOT NULL DEFAULT now(),\n  created_at TIMESTAMP NOT NULL DEFAULT now()\n);\n\n-- congress_members indexes\nCREATE INDEX congress_members_strategy_id_index ON congress_members(strategy_id);\nCREATE INDEX congress_members_first_index ON congress_members(first);\nCREATE INDEX congress_members_last_index ON congress_members(last);\nCREATE INDEX congress_members_location_index ON congress_members(location);\nCREATE INDEX congress_members_multiplier_index ON congress_members(multiplier);\nCREATE INDEX congress_members_created_at_index ON congress_members(created_at);\n\n-- congress_reports indexes\nCREATE INDEX congress_reports_congress_member_id_index ON congress_reports(congress_member_id);\nCREATE INDEX congress_reports_report_type_index ON congress_reports(report_type);\nCREATE INDEX congress_reports_doc_id_index ON congress_reports(doc_id);\nCREATE INDEX congress_reports_filed_at_index ON congress_reports(filed_at);\nCREATE INDEX congress_reports_created_at_index ON congress_reports(created_at);\n```\n\n- **`congress_members`** — Tracks each congress member, their linked AlphaInsider strategy, and a position multiplier\n- **`congress_reports`** — Stores individual filing records (FD and PTR) with extracted holdings as JSONB, linked to the member who filed them\n\n### AlphaInsider API Endpoints\n\nThe workflow interacts with these AlphaInsider endpoints:\n\n| Endpoint | Method | Purpose |\n|---|---|---|\n| `/api/getAllStocks` | GET | Fetch all tradeable stocks (weekly refresh) |\n| `/api/searchStocks` | POST | Look up securities by name or symbol |\n| `/api/getStrategyValues` | GET | Fetch current strategy configuration |\n| `/api/getPositions` | GET | Fetch current portfolio holdings |\n| `/api/newOrderAllocations` | POST | Execute buy/sell/close orders |\n| `/api/newPost` | POST | Create an audience post |\n| `/api/newStrategy` | POST | Create a new strategy |\n| `/api/getExchangeStatus` | GET | Check if the exchange is currently open |\n",
        "height": 4896,
        "width": 1328,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -896,
        -240
      ],
      "typeVersion": 1,
      "id": "33e6898a-7821-412f-b8ff-9f75e843b400",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 10
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        656,
        1200
      ],
      "id": "a577dd20-30e7-41b3-9e92-07a702ed2659",
      "name": "Schedule Reports"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        656,
        688
      ],
      "id": "a33a5407-4d20-4ef7-bf1f-d54e682ae07d",
      "name": "Schedule Stocks"
    },
    {
      "parameters": {
        "url": "https://alphainsider.com/api/getExchangeStatus",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        800,
        2592
      ],
      "id": "66a402ef-d2cb-48ac-888f-3d654a17340b",
      "name": "Exchange Status1"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "965b6d95-f191-4dfd-b6d8-d320c52ab1ef",
              "leftValue": "={{ $json.response.stock }}",
              "rightValue": "open",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        976,
        2592
      ],
      "id": "44418cb9-c0ee-42e6-a82e-60fb6cdef4d8",
      "name": "Check Exchange Status1"
    },
    {
      "parameters": {
        "model": "grok-4-fast-reasoning",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatXAiGrok",
      "typeVersion": 1,
      "position": [
        5264,
        2816
      ],
      "id": "70766565-2511-41a4-bace-4153f7f5d678",
      "name": "xAI Grok Chat Model",
      "credentials": {
        "xAiApi": {
          "id": "aMGKfynprEMpb2TT",
          "name": "xAI"
        }
      }
    },
    {
      "parameters": {
        "model": "grok-4-fast-reasoning",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatXAiGrok",
      "typeVersion": 1,
      "position": [
        2624,
        3056
      ],
      "id": "e652a7f3-ecfb-45c4-90ab-60462f649dbe",
      "name": "xAI Grok Chat Model1",
      "credentials": {
        "xAiApi": {
          "id": "aMGKfynprEMpb2TT",
          "name": "xAI"
        }
      }
    },
    {
      "parameters": {
        "model": "grok-4-fast-reasoning",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatXAiGrok",
      "typeVersion": 1,
      "position": [
        2640,
        2144
      ],
      "id": "83eb0074-e780-4a8b-a63b-98f2a1236be0",
      "name": "xAI Grok Chat Model2",
      "credentials": {
        "xAiApi": {
          "id": "aMGKfynprEMpb2TT",
          "name": "xAI"
        }
      }
    },
    {
      "parameters": {
        "model": "grok-4-fast-reasoning",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatXAiGrok",
      "typeVersion": 1,
      "position": [
        2640,
        1568
      ],
      "id": "d2ab18ba-9765-4e30-9670-a733e227306e",
      "name": "xAI Grok Chat Model3",
      "credentials": {
        "xAiApi": {
          "id": "aMGKfynprEMpb2TT",
          "name": "xAI"
        }
      }
    },
    {
      "parameters": {
        "errorMessage": "An error occured!"
      },
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        6128,
        2784
      ],
      "id": "cc742781-d727-4822-862b-88554b38854b",
      "name": "Stop and Error",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "jsCode": "const holdings = JSON.parse($('Parse Congress Holdings').first().json.holdings)\n\nreturn [{json: {holdings}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4656,
        2960
      ],
      "id": "9d864ebb-aced-4807-8391-83d8b7965f93",
      "name": "Return Empty Holdings"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "cf80c1a1-a979-4e5b-bf46-a2648fbb9c56",
              "leftValue": "={{ $('Parse Congress Holdings').item.json.report_type }}",
              "rightValue": "O",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4464,
        2768
      ],
      "id": "c1b036d6-e975-422a-90f9-045571a2bd7d",
      "name": "If FD Report"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://alphainsider.com/api/newOrderAllocations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"strategy_id\": $('Parse Congress Holdings').item.json.strategy_id, \"allocations\": [] } }}",
        "options": {}
      },
      "id": "38ed567f-8ab8-46b9-b30b-d61737ff6ae9",
      "name": "Create Empty Orders",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4656,
        2752
      ],
      "executeOnce": true,
      "credentials": {
        "httpBearerAuth": {
          "id": "8bECTX76NIRrpJ6Z",
          "name": "CongressTracker"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "37b586ad-ecf1-476c-b2c8-d07ef0fcd342",
              "leftValue": "={{ JSON.parse($('Parse Congress Holdings').item.json.holdings) }}",
              "rightValue": 0,
              "operator": {
                "type": "array",
                "operation": "lengthGt",
                "rightType": "number"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4304,
        2576
      ],
      "id": "92b2a760-e42e-43a6-8379-07ae00c89b6b",
      "name": "If New Trades",
      "alwaysOutputData": false
    }
  ],
  "pinData": {
    "Congress Trigger": [
      {
        "json": {
          "first": "Ed",
          "last": "Case",
          "location": "D-HI-1",
          "doc_id": "10066777",
          "report_type": "O",
          "filed_at": "5/5/2025",
          "year": 2024,
          "congress_member_id": "c2a4s27kkjg",
          "strategy_id": "uC1mdgYp-YvlaZHFi6BsI",
          "multiplier": "3551589.344832070600000"
        }
      }
    ]
  },
  "connections": {
    "Download FD Index ZIP": {
      "main": [
        [
          {
            "node": "Unzip FD Index1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculator1": {
      "ai_tool": [
        [
          {
            "node": "Extract from PTR",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Calculator2": {
      "ai_tool": [
        [
          {
            "node": "Extract from PTR (Vision Fallback)",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Unzip FD Index1": {
      "main": [
        [
          {
            "node": "Read XML Index1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read XML Index1": {
      "main": [
        [
          {
            "node": "Find Latest Docs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download PTR PDF": {
      "main": [
        [
          {
            "node": "Extract Text from PTR PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "Download PTR PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Select PTRs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download FD PDF": {
      "main": [
        [
          {
            "node": "Extract Text from FD PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Congress Trigger": {
      "main": [
        [
          {
            "node": "Exchange Status1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Text from FD PDF": {
      "main": [
        [
          {
            "node": "Has FD Text?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Text from PTR PDF": {
      "main": [
        [
          {
            "node": "Has PTR Text?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has PTR Text?": {
      "main": [
        [
          {
            "node": "Extract from PTR",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract from PTR (Vision Fallback)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from PTR": {
      "main": [
        [
          {
            "node": "Normalize PTR Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from PTR (Vision Fallback)": {
      "main": [
        [
          {
            "node": "Normalize PTR Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize PTR Output": {
      "main": [
        [
          {
            "node": "Parse Congress Holdings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculator3": {
      "ai_tool": [
        [
          {
            "node": "Extract from FD (Vision Fallback)",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Has FD Text?": {
      "main": [
        [
          {
            "node": "Extract from FD",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract from FD (Vision Fallback)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from FD": {
      "main": [
        [
          {
            "node": "Normalize FD Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from FD (Vision Fallback)": {
      "main": [
        [
          {
            "node": "Normalize FD Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Members": {
      "main": [
        [
          {
            "node": "Lookback Years",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Congress Values": {
      "main": [
        [
          {
            "node": "Insert Reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Latest Docs": {
      "main": [
        [
          {
            "node": "Has Strategy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Strategy Value": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculator4": {
      "ai_tool": [
        [
          {
            "node": "Extract from FD",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Global Settings": {
      "main": [
        [
          {
            "node": "Get Positions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Position Percents": {
      "main": [
        [
          {
            "node": "Parse Stock Allocations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Stock Allocations": {
      "main": [
        [
          {
            "node": "Parse Positions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Stocks": {
      "ai_tool": [
        [
          {
            "node": "Parse Stock Allocations",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Calculator": {
      "ai_tool": [
        [
          {
            "node": "Parse Stock Allocations",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Parse Orders Allocations": {
      "main": [
        [
          {
            "node": "Verify Stocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Reports": {
      "main": [
        [
          {
            "node": "If New Trades",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Report Switch": {
      "main": [
        [
          {
            "node": "Insert Reports",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Congress Values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Strategy Action Switch": {
      "main": [
        [
          {
            "node": "Parse Orders Allocations",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Post",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize FD Output": {
      "main": [
        [
          {
            "node": "Parse Congress Holdings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Congress Holdings": {
      "main": [
        [
          {
            "node": "Report Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select PTRs": {
      "main": [
        [
          {
            "node": "Combine PTR Holdings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine PTR Holdings": {
      "main": [
        [
          {
            "node": "Download FD PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Positions": {
      "main": [
        [
          {
            "node": "Strategy Action Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookback Years": {
      "main": [
        [
          {
            "node": "Download FD Index ZIP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Members": {
      "main": [
        [
          {
            "node": "New Strategy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookup Agent": {
      "main": [
        [
          {
            "node": "Parse Members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Members": {
      "main": [
        [
          {
            "node": "Combine Member Docs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New Strategy": {
      "main": [
        [
          {
            "node": "Upsert Members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Member Docs": {
      "main": [
        [
          {
            "node": "Call Congress Trigger - New",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Positions": {
      "main": [
        [
          {
            "node": "Parse Position Percents",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Lookup Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Congress Trigger - New": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Congress Trigger - Update": {
      "main": [
        [
          {
            "node": "Loop Over Reports",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Delete Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Orders": {
      "main": [
        []
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Parse Stock Allocations",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Has Strategy": {
      "main": [
        [
          {
            "node": "Loop Over Reports",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Exchange Is Open": {
      "main": [
        [
          {
            "node": "Get Members",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Exchange Status": {
      "main": [
        [
          {
            "node": "Check If Exchange Is Open",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Report": {
      "main": [
        [
          {
            "node": "Loop Over Reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Reports": {
      "main": [
        [],
        [
          {
            "node": "Call Congress Trigger - Update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create stocks table": {
      "main": [
        [
          {
            "node": "Get All Stocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Stocks": {
      "main": [
        [
          {
            "node": "Parse Stocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Stocks": {
      "main": [
        [
          {
            "node": "Insert row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert row": {
      "main": [
        []
      ]
    },
    "Verify Stocks": {
      "main": [
        [
          {
            "node": "Parse Allocations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Allocations": {
      "main": [
        [
          {
            "node": "Create Orders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List data tables": {
      "main": [
        [
          {
            "node": "Parse Tables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete stocks table": {
      "main": [
        [
          {
            "node": "Create stocks table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Tables": {
      "main": [
        [
          {
            "node": "If stocks table exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If stocks table exists": {
      "main": [
        [
          {
            "node": "Delete stocks table",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create stocks table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Reports": {
      "main": [
        [
          {
            "node": "Exchange Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Stocks": {
      "main": [
        [
          {
            "node": "List data tables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Exchange Status1": {
      "main": [
        [
          {
            "node": "Check Exchange Status1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Exchange Status1": {
      "main": [
        [
          {
            "node": "Get Strategy Value",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "xAI Grok Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Parse Stock Allocations",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "xAI Grok Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Extract from FD",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "xAI Grok Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Extract from PTR",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "xAI Grok Chat Model3": {
      "ai_languageModel": [
        [
          {
            "node": "Lookup Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "If FD Report": {
      "main": [
        [
          {
            "node": "Create Empty Orders",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return Empty Holdings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If New Trades": {
      "main": [
        [
          {
            "node": "Global Settings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If FD Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "tags": []
}