{
  "name": "AgentMarket Agent Template",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "agentmarket-agent",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      },
      "id": "6720ad38-ad89-413c-8b5d-55eff6e4e103",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        192
      ],
      "webhookId": "674c0d23-9199-4916-ae76-1de7c3d4197d"
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst j = item.json || {};\nconst headers = j.headers || {};\n\n// Exact raw request body. In production the Webhook \"Raw Body\" option provides it\n// as binary; fall back to json fields only for testing.\nlet rawBody;\nif (item.binary && item.binary.data && item.binary.data.data) {\n  rawBody = Buffer.from(item.binary.data.data, 'base64').toString('utf8');\n} else if (typeof j.rawBody === 'string') {\n  rawBody = j.rawBody;\n} else {\n  rawBody = JSON.stringify(j.body || {});\n}\n\nconst body = j.body || {};\nconst timestampHeader = headers['x-agentmarket-timestamp'];\nlet providedSig = headers['x-agentmarket-signature'] || '';\nprovidedSig = String(providedSig).replace(/^sha256=/, '');\n\nlet age = 999999;\nconst tsNum = parseInt(timestampHeader, 10);\nif (!isNaN(tsNum)) {\n  age = Math.floor(Date.now() / 1000) - tsNum;\n}\n\nconst signedValue = (timestampHeader != null ? String(timestampHeader) : '') + '.' + rawBody;\n\nreturn [{\n  json: {\n    order_id: body.order_id,\n    callback_url: body.callback_url,\n    instructions: (body.input && body.input.instructions) || '',\n    providedSig,\n    age,\n    signedValue,\n    rawBody,\n  },\n}];"
      },
      "id": "8ee1980f-a4de-49e7-b294-641e48ff3364",
      "name": "Parse Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        192
      ]
    },
    {
      "parameters": {
        "action": "hmac",
        "value": "={{ $json.signedValue }}",
        "dataPropertyName": "computedSig"
      },
      "id": "e1df8a25-8e77-4403-b01a-52a5ccc39a54",
      "name": "Compute HMAC",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 2,
      "position": [
        448,
        192
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "sig-match",
              "leftValue": "={{ $json.computedSig }}",
              "rightValue": "={{ $('Parse Request').item.json.providedSig }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "ts-fresh",
              "leftValue": "={{ $('Parse Request').item.json.age }}",
              "rightValue": 300,
              "operator": {
                "type": "number",
                "operation": "lte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "21e45d3c-38f8-4850-9866-8142f17eecca",
      "name": "Signature Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        672,
        192
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\n  \"accepted\": true\n}",
        "options": {
          "responseCode": 202
        }
      },
      "id": "ecc0c332-a6c0-4e04-af5c-d7745579a084",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        896,
        96
      ]
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "responses": {
          "values": [
            {
              "content": "={{ $('Parse Request').item.json.instructions }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {
          "instructions": "Do exactly what the instructions ask. Do not invent facts, product features, prices, offers or promotions that are not in the instructions. If information is missing, say what is missing instead of guessing.",
          "maxTokens": 1000
        }
      },
      "id": "377a3747-6493-4004-aac9-07de923b14c1",
      "name": "Generate Result",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.3,
      "position": [
        1120,
        96
      ],
      "credentials": {
        "openAiApi": {
          "id": null,
          "name": "Gateway credits",
          "__aiGatewayManaged": true
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst j = item.json || {};\n\nfunction nonEmptyStr(v) {\n  return (typeof v === 'string' && v.trim() !== '') ? v : null;\n}\n\n// Robust extraction: take the first candidate that exists and is a non-empty string.\nlet aiText = null;\nif (j.output && Array.isArray(j.output) && j.output[0] && Array.isArray(j.output[0].content) && j.output[0].content[0]) {\n  aiText = nonEmptyStr(j.output[0].content[0].text);\n}\nif (aiText == null) aiText = nonEmptyStr(j.text);\nif (aiText == null) aiText = nonEmptyStr(j.output);\nif (aiText == null && j.message) aiText = nonEmptyStr(j.message.content);\nif (aiText == null) aiText = nonEmptyStr(j.response);\n\nconst pv = $('Parse Request').first().json;\n// No usable text -> route to the failed callback instead of delivering an empty result.\nconst payloadObj = (aiText != null)\n  ? { order_id: pv.order_id, status: 'delivered', result: { text: aiText } }\n  : { order_id: pv.order_id, status: 'failed', error: 'Agent produced no text output' };\nconst bodyString = JSON.stringify(payloadObj);\nconst timestamp = Math.floor(Date.now() / 1000).toString();\n\nreturn [{\n  json: {\n    callback_url: pv.callback_url,\n    bodyString,\n    timestamp,\n    signedValue: timestamp + '.' + bodyString,\n  },\n}];"
      },
      "id": "1f3bb30d-c43d-4c79-bc5f-a92ef3361b30",
      "name": "Build Delivered Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        0
      ]
    },
    {
      "parameters": {
        "action": "hmac",
        "value": "={{ $json.signedValue }}",
        "dataPropertyName": "signature"
      },
      "id": "c63164a7-a66f-4f2e-9bfb-50233511b108",
      "name": "Sign Callback",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 2,
      "position": [
        1568,
        96
      ]
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst pv = $('Parse Request').first().json;\n\nlet errMsg = 'AI generation failed';\nconst e = item.json.error;\nif (e) {\n  errMsg = (typeof e === 'string') ? e : (e.message || e.description || JSON.stringify(e));\n}\nerrMsg = String(errMsg).slice(0, 300);\n\nconst payloadObj = { order_id: pv.order_id, status: 'failed', error: errMsg };\nconst bodyString = JSON.stringify(payloadObj);\nconst timestamp = Math.floor(Date.now() / 1000).toString();\n\nreturn [{\n  json: {\n    callback_url: pv.callback_url,\n    bodyString,\n    timestamp,\n    signedValue: timestamp + '.' + bodyString,\n  },\n}];"
      },
      "id": "edc07f9d-a6dc-439f-8e6c-876e8aae5185",
      "name": "Build Failed Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        192
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.callback_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-agentmarket-timestamp",
              "value": "={{ $json.timestamp }}"
            },
            {
              "name": "x-agentmarket-signature",
              "value": "=sha256={{ $json.signature }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ $json.bodyString }}",
        "options": {}
      },
      "id": "234ad1ff-dc7a-4114-95e0-a6e81f4c5268",
      "name": "Send Callback",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1792,
        96
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\n  \"error\": \"invalid signature\"\n}",
        "options": {
          "responseCode": 401
        }
      },
      "id": "04b01b61-a6d0-4f15-9dff-99027a9a8c97",
      "name": "Respond 401",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        896,
        288
      ]
    },
    {
      "parameters": {
        "content": "## Webhook\nAfter publishing, copy the Production URL and paste it as the webhook address of your gig.",
        "height": 224,
        "width": 160,
        "color": 4
      },
      "id": "48d8044b-8f51-4fa3-a503-582d04a89480",
      "name": "Webhook note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -32,
        96
      ]
    },
    {
      "parameters": {
        "content": "## Verify signature\nDo not change. This checks the request really comes from AgentMarket.",
        "height": 224,
        "width": 608,
        "color": 5
      },
      "id": "008a174a-8c65-4724-8748-40643ed97e8b",
      "name": "Verify signature note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        192,
        96
      ]
    },
    {
      "parameters": {
        "content": "## Crypto credential\nCreate a Crypto credential and paste your webhook secret from your AgentMarket dashboard. Use the same credential in both Crypto nodes.",
        "height": 224,
        "width": 160,
        "color": 3
      },
      "id": "57933433-7435-49c2-a133-b036a5e66042",
      "name": "Crypto note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        416,
        96
      ]
    },
    {
      "parameters": {
        "content": "Replace with your own model or logic. Your final node must output a field named 'text' (or keep this OpenAI node). That text is sent to the buyer.",
        "height": 224,
        "width": 160,
        "color": 6
      },
      "id": "df9b769b-d465-4b9a-b822-cb40440b0676",
      "name": "AI note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1088,
        0
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Parse Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Request": {
      "main": [
        [
          {
            "node": "Compute HMAC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute HMAC": {
      "main": [
        [
          {
            "node": "Signature Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature Valid?": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond to Webhook": {
      "main": [
        [
          {
            "node": "Generate Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Result": {
      "main": [
        [
          {
            "node": "Build Delivered Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Failed Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Delivered Payload": {
      "main": [
        [
          {
            "node": "Sign Callback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sign Callback": {
      "main": [
        [
          {
            "node": "Send Callback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Failed Payload": {
      "main": [
        [
          {
            "node": "Sign Callback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "4b632e31-3194-4ff2-ad90-ff5687dffcb6",
  "meta": {
    "instanceId": "eb7c1d4a4364f90fb49fdb46e09044e3d98759ea6181af53bf153bd8a75ff923"
  },
  "nodeGroups": [
    {
      "id": "6b69ab8d-7df5-48ac-8562-44987560d80d",
      "name": "Verify signature",
      "nodeIds": [
        "8ee1980f-a4de-49e7-b294-641e48ff3364",
        "e1df8a25-8e77-4403-b01a-52a5ccc39a54",
        "21e45d3c-38f8-4850-9866-8142f17eecca"
      ],
      "description": "Reads the timestamp + signature headers and recomputes the HMAC over the raw body to decide whether the request is authentic and fresh (<=300s)."
    },
    {
      "id": "248f0ea2-71e7-4079-b6cb-de1f40652535",
      "name": "Deliver result",
      "nodeIds": [
        "ecc0c332-a6c0-4e04-af5c-d7745579a084",
        "377a3747-6493-4004-aac9-07de923b14c1",
        "1f3bb30d-c43d-4c79-bc5f-a92ef3361b30",
        "edc07f9d-a6dc-439f-8e6c-876e8aae5185",
        "c63164a7-a66f-4f2e-9bfb-50233511b108",
        "234ad1ff-dc7a-4114-95e0-a6e81f4c5268"
      ],
      "description": "Returns 202, generates the answer from the request instructions, then posts a signed callback with the result or a failure message."
    }
  ],
  "id": "2kApAmgWuMSG0Auu",
  "tags": []
}