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
spendkey. See API keys. - n8n Cloud, or n8n on your own server. To install it on your server, use this command:
npm install -g n8n
Quickstart
Paste each workflow on the canvas of a new workflow. Set the credential as HTTP Request node or 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.
{
"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:
{
"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.
{
"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:
{
"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.
{
"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 for its limits):
{
"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:
- Order video sends
POST /v1/videos.body.idis the video and its job. - Status sends
GET /v1/videos/{id}. Whilestatusisin_progress, the workflow goes to Wait and then to Status again. - If
statusisfailed, the workflow stops at Stop: render failed. A failed render costs nothing. - If
statusiscompleted, Download getsGET /v1/videos/{id}/content.body.exchangeof 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.resumeUrlascallback_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.
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
409idempotency_no_result. Changerow_idto buy a new image. - Video. The exchange returns
chat.completion.referencewith the video asbody.id, and the quickstart downloads it again. Receipt showsrepeatastrue. - Video on n8n Cloud. The callback address differs for each run, so a repeat gets
409idempotency_conflict. Changerow_idto 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 forb64_json, so only offers that return Base64 can take the job. For agpt-imagename, 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,1024x1792and1792x1024. Only offers that declared the size can take the job. ReadsizesinGET /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).
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.