# n8n

n8n is a workflow automation tool. Its workflows can send text requests and buy images and videos from the exchange.

Use the HTTP Request node for unattended or retried workflows. Use the OpenAI node for a quick start.

|                                 | HTTP Request node                       | OpenAI node                                               |
| ------------------------------- | --------------------------------------- | --------------------------------------------------------- |
| Credential                      | Bearer Auth                             | OpenAI, with the base URL of the exchange                 |
| Text                            | `POST /v1/chat/completions`             | **Message a Model**, which sends `/v1/responses`          |
| Images                          | `POST /v1/images/generations`           | **Generate an Image**                                     |
| Video                           | `POST /v1/videos`, then **Wait**        | **Generate a Video**                                      |
| Job ID and charge               | In the output                           | Text: with **Simplify Output** off. Images and video: no. |
| A retry or a new run of a brief | Buys nothing, with an `Idempotency-Key` | Buys again                                                |
| Image and video size            | Any size that an offer declared         | Image: the seller's default. Video: one of four sizes.    |

## Before you start

- A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md).
- n8n Cloud, or n8n on your own server. To install it on your server, use this command:

```bash
npm install -g n8n
```

## Quickstart

Paste each workflow on the canvas of a new workflow. Set the credential as [HTTP Request node](#http-request-node) or [OpenAI node](#openai-node) tells you, then run it.

### Text

HTTP Request node. Set `prompt` and `model` in **Brief**. **Reply** holds the answer, `job_id` and `charge_usd`.

```json file=liquid-n8n-http-text.json
{
  "name": "Liquid Inference: send a text request",
  "nodes": [
    {
      "parameters": {
        "content": "## Send one text request to Liquid Inference\n1. Open **Ask**. Create a **Bearer Auth** credential named `Liquid`, and paste your key into **Bearer Token**.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Brief**, then run the workflow.\n\n**Reply** holds the answer, with the job id and the charge.",
        "height": 260,
        "width": 520
      },
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -320
      ]
    },
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "prompt",
              "value": "Write one sentence about a red bicycle.",
              "type": "string"
            },
            {
              "name": "model",
              "value": "liquid.auto",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Brief",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://router.inference.ai.exchange/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-title",
              "value": "Liquid n8n workflow"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $json.model, messages: [{ role: 'user', content: $json.prompt }] }) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          },
          "timeout": 300000
        }
      },
      "name": "Ask",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "Liquid"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.statusCode }}",
              "operator": {
                "type": "number",
                "operation": "equals"
              },
              "rightValue": 200
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Answered?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "errorMessage": "=The exchange refused the request: {{ $json.body.error.code }}: {{ $json.body.error.message }}"
      },
      "name": "Stop: refused",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        880,
        140
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "reply",
              "value": "={{ $json.body.choices[0].message.content }}",
              "type": "string"
            },
            {
              "name": "job_id",
              "value": "={{ $json.body.exchange.job_id }}",
              "type": "string"
            },
            {
              "name": "charge_usd",
              "value": "={{ $json.body.exchange.charge.total_usd }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Reply",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        880,
        -40
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Brief": {
      "main": [
        [
          {
            "node": "Ask",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask": {
      "main": [
        [
          {
            "node": "Answered?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answered?": {
      "main": [
        [
          {
            "node": "Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: refused",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
```

OpenAI node:

```json file=liquid-n8n-openai-text.json
{
  "name": "Liquid Inference: send a text request with the OpenAI node",
  "nodes": [
    {
      "parameters": {
        "content": "## Send one text request to Liquid Inference\n1. Open **Message a model**. Select the **OpenAI** credential named `Liquid`, with the base URL of the exchange.\n2. Set a spend limit on the key in the console.\n3. Change the message, then run the workflow.\n\nTo see the job id and the charge, turn off **Simplify Output**.",
        "height": 260,
        "width": 520
      },
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -320
      ]
    },
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "resource": "text",
        "operation": "response",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "liquid.auto"
        },
        "responses": {
          "values": [
            {
              "content": "Write one sentence about a red bicycle."
            }
          ]
        },
        "options": {}
      },
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.2,
      "position": [
        220,
        0
      ],
      "credentials": {
        "openAiApi": {
          "name": "Liquid"
        }
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
```

### Images

HTTP Request node. Set `prompt`, `model` and `size` in **Brief**. If `size` is empty, the seller uses its default. **Receipt** holds the image as the binary `data`, with `job_id` and `charge_usd`.

```json file=liquid-n8n-http-image.json
{
  "name": "Liquid Inference: buy an image",
  "nodes": [
    {
      "parameters": {
        "content": "## Buy one image on Liquid Inference\n1. Open **Buy image**. Create a **Bearer Auth** credential named `Liquid`, and paste your key into **Bearer Token**.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Brief**, then run the workflow.\n\nThe image is the binary `data` of **Receipt**, with the job id and the charge.",
        "height": 300,
        "width": 520
      },
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -360
      ]
    },
    {
      "parameters": {
        "content": "## A retry buys nothing\n**Key** makes an `Idempotency-Key` from the row id and the brief. A retry or a new run of the same brief gets the first job back, not a second image.\n\nChange `row_id` to buy a new image of the same brief.\n\nA repeat or a refusal stops the workflow with a message. To alert a person, set an error workflow in the workflow settings.",
        "height": 300,
        "width": 520
      },
      "name": "Retries",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        560,
        -360
      ]
    },
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "row_id",
              "value": "brief-1",
              "type": "string"
            },
            {
              "name": "prompt",
              "value": "A red bicycle leaning on a brick wall",
              "type": "string"
            },
            {
              "name": "model",
              "value": "liquid.auto",
              "type": "string"
            },
            {
              "name": "size",
              "value": "",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Brief",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "action": "hash",
        "type": "SHA256",
        "value": "={{ [$json.row_id, $json.prompt, $json.model, $json.size].join('|') }}",
        "dataPropertyName": "key"
      },
      "name": "Key",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 1,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://router.inference.ai.exchange/v1/images/generations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.key }}"
            },
            {
              "name": "x-liquid-cap-usd",
              "value": "0.50"
            },
            {
              "name": "x-title",
              "value": "Liquid n8n workflow"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify(Object.assign({ model: $json.model, prompt: $json.prompt, n: 1 }, $json.size ? { size: $json.size } : {})) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          },
          "timeout": 300000
        }
      },
      "name": "Buy image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "Liquid"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.statusCode }}",
              "operator": {
                "type": "number",
                "operation": "equals"
              },
              "rightValue": 200
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Bought?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.body.object }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "chat.completion.reference"
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Repeat?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        -120
      ]
    },
    {
      "parameters": {
        "errorMessage": "=This brief was bought before, as job {{ $json.body.exchange.job_id }}. The exchange does not keep images, so this run has no file. Change row_id in Brief to buy a new image."
      },
      "name": "Stop: already bought",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1320,
        -240
      ]
    },
    {
      "parameters": {
        "errorMessage": "=The exchange refused the request: {{ $json.body.error.code }}: {{ $json.body.error.message }}"
      },
      "name": "Stop: refused",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1100,
        140
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.body.data[0].b64_json }}",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Bytes?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1320,
        -40
      ]
    },
    {
      "parameters": {
        "operation": "toBinary",
        "sourceProperty": "body.data[0].b64_json",
        "options": {
          "mimeType": "={{ $json.body.data[0].mime_type }}"
        }
      },
      "name": "To file",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        1540,
        -120
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.body.data[0].url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "name": "Download",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        40
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "job_id",
              "value": "={{ $('Buy image').item.json.body.exchange.job_id }}",
              "type": "string"
            },
            {
              "name": "charge_usd",
              "value": "={{ $('Buy image').item.json.body.exchange.charge.total_usd }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "include": "except",
        "excludeFields": "body, headers, statusCode, statusMessage",
        "options": {
          "stripBinary": false
        }
      },
      "name": "Receipt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1760,
        -40
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Brief": {
      "main": [
        [
          {
            "node": "Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Key": {
      "main": [
        [
          {
            "node": "Buy image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buy image": {
      "main": [
        [
          {
            "node": "Bought?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bought?": {
      "main": [
        [
          {
            "node": "Repeat?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: refused",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Repeat?": {
      "main": [
        [
          {
            "node": "Stop: already bought",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Bytes?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bytes?": {
      "main": [
        [
          {
            "node": "To file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "To file": {
      "main": [
        [
          {
            "node": "Receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download": {
      "main": [
        [
          {
            "node": "Receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
```

OpenAI node:

```json file=liquid-n8n-openai-image.json
{
  "name": "Liquid Inference: buy an image with the OpenAI node",
  "nodes": [
    {
      "parameters": {
        "content": "## Buy one image on Liquid Inference\n1. Open **Generate image**. Select the **OpenAI** credential named `Liquid`, with the base URL of the exchange.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Generate image**, then run the workflow.\n\nThe image is the binary `data` of **Generate image**. Each run buys a new image. Do not turn on **Retry On Fail**: a retry can buy a second image.",
        "height": 300,
        "width": 520
      },
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -360
      ]
    },
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "generate",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "liquid.auto"
        },
        "prompt": "A red bicycle leaning on a brick wall",
        "options": {}
      },
      "name": "Generate image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.2,
      "position": [
        220,
        0
      ],
      "credentials": {
        "openAiApi": {
          "name": "Liquid"
        }
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Generate image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
```

### Video

HTTP Request node. Set `prompt`, `model`, `seconds` and `size` in **Brief**, and the credential in **Order video**, **Status** and **Download**. See [Wait for a video](#4-wait-for-a-video).

```json file=liquid-n8n-http-video.json
{
  "name": "Liquid Inference: buy a video",
  "nodes": [
    {
      "parameters": {
        "content": "## Buy one video on Liquid Inference\n1. Open **Order video**. Create a **Bearer Auth** credential named `Liquid`, and paste your key into **Bearer Token**. Select the same credential in **Status** and **Download**.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Brief**, then run the workflow.\n\nThe video is the binary `data` of **Receipt**, with the job id and the charge.",
        "height": 320,
        "width": 520
      },
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -380
      ]
    },
    {
      "parameters": {
        "content": "## Wait for the render\nA render takes from less than a minute to several minutes. If n8n has a public `https` address, as on n8n Cloud, **Order video** sends `$execution.resumeUrl` as `callback_url`. The exchange calls it when the job ends, and **Wait** resumes at once. Otherwise **Wait** resumes after 20 seconds, and **Status** reads the job again.\n\n**Key** makes an `Idempotency-Key` from the row id and the brief, so a retry buys nothing. Change `row_id` to buy a new video of the same brief.",
        "height": 320,
        "width": 520
      },
      "name": "Waiting",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        560,
        -380
      ]
    },
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "row_id",
              "value": "brief-1",
              "type": "string"
            },
            {
              "name": "prompt",
              "value": "A red paper boat on a stream",
              "type": "string"
            },
            {
              "name": "model",
              "value": "seedance-1-5-pro",
              "type": "string"
            },
            {
              "name": "seconds",
              "value": "5",
              "type": "string"
            },
            {
              "name": "size",
              "value": "",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Brief",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "action": "hash",
        "type": "SHA256",
        "value": "={{ [$json.row_id, $json.prompt, $json.model, $json.seconds, $json.size].join('|') }}",
        "dataPropertyName": "key"
      },
      "name": "Key",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 1,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://router.inference.ai.exchange/v1/videos",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.key }}"
            },
            {
              "name": "x-liquid-cap-usd",
              "value": "1.50"
            },
            {
              "name": "x-title",
              "value": "Liquid n8n workflow"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify(Object.assign({ model: $json.model, prompt: $json.prompt }, $json.seconds ? { seconds: $json.seconds } : {}, $json.size ? { size: $json.size } : {}, $execution.resumeUrl.startsWith('https://') ? { callback_url: $execution.resumeUrl } : {})) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          },
          "timeout": 300000
        }
      },
      "name": "Order video",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "Liquid"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.statusCode }}",
              "operator": {
                "type": "number",
                "operation": "equals"
              },
              "rightValue": 200
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Placed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "errorMessage": "=The exchange refused the request: {{ $json.body.error.code }}: {{ $json.body.error.message }}"
      },
      "name": "Stop: refused",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1100,
        160
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "video_id",
              "value": "={{ $json.body.id }}",
              "type": "string"
            },
            {
              "name": "wait_seconds",
              "value": "={{ $json.body.callback ? 600 : 60 }}",
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "name": "Job",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1100,
        -40
      ]
    },
    {
      "parameters": {
        "url": "=https://router.inference.ai.exchange/v1/videos/{{ $('Job').first().json.video_id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {}
      },
      "name": "Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        -40
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "Liquid"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "rightValue": "in_progress"
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Ended?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        -40
      ]
    },
    {
      "parameters": {
        "resume": "webhook",
        "httpMethod": "POST",
        "limitWaitTime": true,
        "limitType": "afterTimeInterval",
        "resumeAmount": "={{ $('Job').first().json.wait_seconds }}",
        "resumeUnit": "seconds",
        "options": {}
      },
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1540,
        180
      ],
      "webhookId": "liquid-video-wait"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "completed"
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "name": "Completed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        -120
      ]
    },
    {
      "parameters": {
        "errorMessage": "=The render of {{ $json.id }} failed: {{ $json.error.code }}: {{ $json.error.message }}. A failed render costs nothing."
      },
      "name": "Stop: render failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1980,
        40
      ]
    },
    {
      "parameters": {
        "url": "=https://router.inference.ai.exchange/v1/videos/{{ $('Job').first().json.video_id }}/content",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          },
          "timeout": 300000
        }
      },
      "name": "Download",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1980,
        -200
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "Liquid"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "job_id",
              "value": "={{ $('Status').last().json.id }}",
              "type": "string"
            },
            {
              "name": "seconds",
              "value": "={{ $('Status').last().json.seconds }}",
              "type": "string"
            },
            {
              "name": "charge_usd",
              "value": "={{ $('Status').last().json.exchange.charge.total_usd }}",
              "type": "string"
            },
            {
              "name": "repeat",
              "value": "={{ $('Order video').first().json.body.object === 'chat.completion.reference' }}",
              "type": "boolean"
            }
          ]
        },
        "options": {
          "stripBinary": false
        },
        "includeOtherFields": true,
        "include": "selected",
        "includeFields": "model"
      },
      "name": "Receipt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2200,
        -200
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Brief": {
      "main": [
        [
          {
            "node": "Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Key": {
      "main": [
        [
          {
            "node": "Order video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Order video": {
      "main": [
        [
          {
            "node": "Placed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Placed?": {
      "main": [
        [
          {
            "node": "Job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: refused",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Job": {
      "main": [
        [
          {
            "node": "Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Status": {
      "main": [
        [
          {
            "node": "Ended?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ended?": {
      "main": [
        [
          {
            "node": "Completed?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Completed?": {
      "main": [
        [
          {
            "node": "Download",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: render failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download": {
      "main": [
        [
          {
            "node": "Receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
```

OpenAI node (see [Generate a video](#4-generate-a-video) for its limits):

```json file=liquid-n8n-openai-video.json
{
  "name": "Liquid Inference: buy a video with the OpenAI node",
  "nodes": [
    {
      "parameters": {
        "content": "## Buy one video on Liquid Inference\n1. Open **Generate video**. Select the **OpenAI** credential named `Liquid`, with the base URL of the exchange.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Generate video**, then run the workflow.\n\nThe video is the binary `data` of **Generate video**. Each run buys a new video. Do not turn on **Retry On Fail**: a retry can buy a second video.",
        "height": 300,
        "width": 520
      },
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -360
      ]
    },
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "resource": "video",
        "operation": "generate",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "p-video"
        },
        "prompt": "A red paper boat on a stream",
        "seconds": 5,
        "size": "1280x720",
        "options": {
          "waitTime": 1200
        }
      },
      "name": "Generate video",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.2,
      "position": [
        220,
        0
      ],
      "credentials": {
        "openAiApi": {
          "name": "Liquid"
        }
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Generate video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
```

## HTTP Request node

### 1. Create a Bearer Auth credential

Set **Authentication** to **Generic Credential Type**, and **Generic Auth Type** to **Bearer Auth**. Create a credential named `Liquid`, and paste the key into **Bearer Token**. Do not type `Bearer`, because n8n adds it.

Do not put the key in a header of the node, because n8n writes node headers into each export.

### 2. Set the request

Set **Method** to `POST`. Set **URL** to one of these addresses.

Text:

```
https://router.inference.ai.exchange/v1/chat/completions
```

Images:

```
https://router.inference.ai.exchange/v1/images/generations
```

Video:

```
https://router.inference.ai.exchange/v1/videos
```

| Field                        | Value                                                                                                                                             |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Send Body**                | JSON. Text: `model`, `messages`. Images: `model`, `prompt`, `n` as `1`, optional `size`. Video: `model`, `prompt`, optional `seconds` and `size`. |
| **Send Headers**             | `x-liquid-cap-usd`: the most one request can cost, such as `0.50` (optional). `Idempotency-Key`: the same value for one request.                  |
| **Options › Response**       | Turn on **Include Response Headers and Status** and **Never Error**.                                                                              |
| **Options › Timeout**        | `300000`, because an image can take minutes.                                                                                                      |
| **Settings › Retry On Fail** | On, only with an `Idempotency-Key`.                                                                                                               |

The image and video quickstarts make the key in their **Key** node, as a SHA-256 of `row_id` and the brief.

### 3. Read the answer

| Status and body                                       | Meaning                                                  |
| ----------------------------------------------------- | -------------------------------------------------------- |
| `200`, with `body.choices[0].message.content`         | The text answer.                                         |
| `200`, with `body.data[0].b64_json`                   | The image as Base64. Convert it to a file.               |
| `200`, with `body.data[0].url`                        | A link to the image. Download it without the credential. |
| `200`, with `body.object` `chat.completion.reference` | A repeat of a used key. It has the job and no answer.    |
| Not `200`                                             | A refusal. `body.error.code` gives the cause.            |

`body.exchange.job_id` is the job, and `body.exchange.charge.total_usd` is the charge.

### 4. Wait for a video

A video request returns when a provider accepts the job. The video quickstart then does these steps:

1. **Order video** sends `POST /v1/videos`. `body.id` is the video and its job.
2. **Status** sends `GET /v1/videos/{id}`. While `status` is `in_progress`, the workflow goes to **Wait** and then to **Status** again.
3. If `status` is `failed`, the workflow stops at **Stop: render failed**. A failed render costs nothing.
4. If `status` is `completed`, **Download** gets `GET /v1/videos/{id}/content`. `body.exchange` of **Status** holds the charge.

**Wait** resumes when the exchange calls the workflow, or when its time limit ends:

- If the address of n8n starts with `https://`, as on n8n Cloud, **Order video** sends `$execution.resumeUrl` as `callback_url`. The time limit is 10 minutes.
- If not, the exchange does not call the workflow. The time limit is one minute.

The exchange refuses a `callback_url` that is not a public `https` address, such as `http://localhost:5678`, and no job starts. On your own server, set `WEBHOOK_URL` to the public `https` address of n8n.

The callback only resumes the workflow, so you do not need the `signing_secret`. See [Video generation](https://inference.ai.exchange/docs/video-generation.md).

### Retries and repeats

The exchange keeps a used `Idempotency-Key` for 30 days. A retry or a new run with the same key buys nothing. The text quickstart sends no key, so each run is a new request.

- **Images.** A finished first request stops the quickstart at **Stop: already bought**, because the exchange keeps no images. An unfinished one returns `409` `idempotency_no_result`. Change `row_id` to buy a new image.
- **Video.** The exchange returns `chat.completion.reference` with the video as `body.id`, and the quickstart downloads it again. **Receipt** shows `repeat` as `true`.
- **Video on n8n Cloud.** The callback address differs for each run, so a repeat gets `409` `idempotency_conflict`. Change `row_id` to buy a new video.

## OpenAI node

### 1. Create an OpenAI credential

Click **+** in the left rail, then **Credential**. Select **OpenAI**, not **Azure OpenAI**, and name it `Liquid`. Set **API Key** and **Base URL**. Leave **Organization ID** empty and **Add Custom Header** off.

```
sk-liquid-...
```

```
https://router.inference.ai.exchange/v1
```

Keep `/v1` at the end of the base URL. Each OpenAI node that uses the credential then goes through the exchange. Click **Save**. n8n tests it with `GET /v1/models`.

### 2. Send a text request

Add an **OpenAI** node, select **Message a Model** and the credential. For **Model**, select **By ID** and enter a name from `GET /v1/models`. Enter a message.

```
liquid.auto
```

To see the job ID and the charge, turn off **Simplify Output**. The output then has the `exchange` block.

### 3. Generate an image

Add an **OpenAI** node, select **Generate an Image** and the credential. For **Model**, select **By ID** and enter a name from `GET /v1/models?kind=image`. Enter a prompt. The node holds the image as the binary `data`.

- It has no size option for a model by ID, so the seller uses its default.
- For a name that does not start with `gpt-image`, it asks for `b64_json`, so only offers that return Base64 can take the job. For a `gpt-image` name, an offer that returns a link makes the node fail.

### 4. Generate a video

Add an **OpenAI** node, select **Generate a Video** and the credential. For **Model**, select **By ID** and enter a name from `GET /v1/models?kind=video`. Enter a prompt. The node sends `POST /v1/videos`, reads the status every 10 seconds, and downloads the file as the binary `data`.

- It always sends **Size**, and gives only `720x1280`, `1280x720`, `1024x1792` and `1792x1024`. Only offers that declared the size can take the job. Read `sizes` in `GET /v1/exchange/models?kind=video`.
- It always sends **Seconds**. Use a length that an offer declared.
- It stops after **Options › Wait Timeout**, 300 seconds by default. The job continues and the exchange charges for it, but the node has no file. The quickstart sets 1200 seconds.

The OpenAI node gives no job ID or charge for images and video (see the **Receipts** page). It sends no `Idempotency-Key`. Do not turn on **Retry On Fail**, because a retry can buy a second answer, image or video.

## Notes

The cap limits each request, not a workflow. To limit total spend, set a spending limit on the key ([API keys](https://inference.ai.exchange/docs/keys.md)).

## Troubleshooting

| Symptom                                        | Cause and fix                                                                                              |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `401`, or `404` when you save the credential   | Paste the key again. Keep `/v1` at the end of the base URL.                                                |
| `404 model_not_found`                          | No seller lists the model now. Use an exact name from `GET /v1/models`.                                    |
| `402 insufficient_balance`                     | Add credit to the workspace or check the spend limit of the key.                                           |
| `400 unsupported_parameter`                    | No offer for the model takes the value in `param`, usually `size`. Leave `size` empty.                     |
| `503` with `cap_exceeds_buyer_cap`             | Each offer costs more than the cap. Increase `x-liquid-cap-usd`.                                           |
| **Stop: refused** with `idempotency_no_result` | The first request with this key has not finished, or the exchange refused it. Change `row_id`, then rerun. |
| **Stop: refused** with `callback_url`          | The `https` address of n8n is not public. Set `WEBHOOK_URL`.                                               |
| **Stop: render failed**                        | The render failed and costs nothing. Rerun with a new `row_id`, or change `model`.                         |
| **Generate a Video** stops with `Timeout`      | The render took longer than **Wait Timeout**. Increase it. The exchange charged for the video.             |

See [Errors](https://inference.ai.exchange/docs/errors.md).
