# Sell video

This page is for a provider that renders video. Use the reference agent if your upstream has the three OpenAI video routes below. Then register the deployment and test it.

## Use an agent

A video job needs an agent. The [OpenAI-compatible path](https://inference.ai.exchange/docs/provider-openai.md) does not carry video jobs, and the exchange sends them to the next offer.

A render takes minutes, and the buyer downloads the file later. The agent does three jobs that a plain API cannot do:

- It starts the render and waits for it to end.
- It reports the seconds that you rendered, so that the job settles.
- It names the file to the exchange and serves the file when the buyer downloads it.

Choose one of two agents:

| Choice                       | Use it when                                                                                                                                    |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| The reference provider agent | Your upstream has the OpenAI video routes that this page describes. You write no code.                                                         |
| Your own agent               | Your upstream has other routes. Follow the [Video section of the dispatch page](https://inference.ai.exchange/docs/provider-dispatch.md#video) and send `video_ready` yourself. |

The rest of this page is for the reference agent.

## Give the agent these routes

Set the base URL of your upstream in `LIQUID_UPSTREAM_BASE_URL`. The agent adds the paths below to it and sends your key as `Authorization: Bearer <key>`.

| The agent calls                  | Your answer                                        |
| -------------------------------- | -------------------------------------------------- |
| `POST {base}/videos`             | A JSON job with an `id`.                           |
| `GET {base}/videos/{id}`         | The job with its `status`.                         |
| `GET {base}/videos/{id}/content` | The file, with a `Content-Type` that is `video/*`. |

### Create a job

The agent sends only the fields in this example.

```json
{
  "model": "acme-video-1",
  "prompt": "A trading floor made of water",
  "seconds": "8",
  "size": "1280x720",
  "input_reference": { "image_url": "https://example.com/start.png" }
}
```

| Field             | Value                                                                |
| ----------------- | -------------------------------------------------------------------- |
| `model`           | The upstream model of the deployment.                                |
| `seconds`         | A string, as in the OpenAI API.                                      |
| `size`            | Present only when the buyer asked for a size.                        |
| `input_reference` | Present only when the buyer gave a start image. See the rules below. |

The agent sends at most one start image. It accepts an image with no frame type or with the frame `first_frame`. It fails the job for the frame `last_frame`.

Answer `2xx` with a JSON body that has an `id`. A status that is not `2xx`, or a body with no `id`, ends the attempt.

### Report the status

The agent asks for the job at a fixed interval. The `status` must be one of these values:

| `status`      | Meaning                                            |
| ------------- | -------------------------------------------------- |
| `queued`      | The job waits. The agent asks again.               |
| `in_progress` | The job renders. The agent asks again.             |
| `completed`   | The file is ready.                                 |
| `failed`      | The job failed. Put the reason in `error.message`. |

Any other value fails the job.

- A `5xx` answer or a network error on a poll is tried again a few times. Then the job fails.
- A `4xx` answer fails the job at once.

When the job is `completed`, you can add `expires_at`, in Unix seconds. It is the time when you delete the file.

### Serve the file

The agent reads the headers of the file before it tells the exchange that the video exists. Then the buyer is charged. Make sure that `GET {base}/videos/{id}/content` works when the status is `completed`.

- Send a `Content-Type` that starts with `video/`. Any other type, or an empty file, fails the job.
- Keep the file until `expires_at`. The buyer can download it after the job ends.

### Bill fewer seconds

The agent charges the seconds that the buyer asked for. To charge fewer, add this header to the `POST` answer:

```text
x-new-api-other-ratios: {"seconds": 6}
```

The agent rounds the value up. It never charges more seconds than the buyer asked for, because the exchange does not pay for them.

## Frames and other references

The OpenAI video routes take one start image. The reference agent does not send a last frame, audio or a video reference on this route.

The exchange sends your deployment only the frames that you declared in `supported_frame_images`. Declare none that the route cannot read.

To read more, write your own agent. A part of the dispatch has `frame_type` when it is a frame image, and you map it to your upstream fields. See [Frame images](https://inference.ai.exchange/docs/provider-models.md#frame-images).

## Declare the controls

A buyer's `seed`, `creativity`, `upscale_factor` and `previous_job_id` reach only an offer that declared `video.seed`, `video.creativity`, `video.upscale_factor` or `video.continue`. The first-party OpenAI adapter declares `video.seed` when the route has a seed rule. It declares no other video control. To sell the others, write your own agent. See [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md).

## Register the deployment

Register the deployment with `kind` set to `video_generation`. Make `deployment_name` and `model` equal, because the agent asks the upstream for that name. The lengths and sizes you list are the only ones that can win.

```bash
curl https://router.inference.ai.exchange/v1/provider/models \
  -H "Authorization: Bearer $LIQUID_PROVIDER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"deployment_name":"acme-video-1",
       "model":"acme-video-1",
       "kind":"video_generation",
       "precision":"provider-managed","region":"us",
       "max_input_tokens":2000,"max_output_tokens":1,
       "output_modalities":[{"type":"video","seconds":[4,8],
                             "sizes":["1280x720"],"formats":["video/mp4"]}]}'
```

[Register deployments](https://inference.ai.exchange/docs/provider-models.md) lists every field.

Quote an `output_video` line in `pricing` for the deployment. The charge is that rate multiplied by the seconds that the buyer asked for. The exchange knows the charge before dispatch, so the price cap is exactly the charge. [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md#a-video-model) has an example with variants.

If your upstream lets the buyer choose the sound or the resolution, state `audio` and `resolutions` in the `video` output and quote `output_video` lines with a `variant` for the options that change the price. The buyer's choice is in `audio` and `resolution` of the `video` object of the dispatch. See [Sound, resolution and price](#sound-resolution-and-price).

## Continue a video

A buyer can send `previous_job_id` to continue an earlier video. Declare `video.continue` only if your upstream can do this.

- The dispatch holds `video.previous_job_id`. It is the job id that the exchange sent to you in the dispatch of the earlier video.
- The exchange checks that the earlier job completed and belongs to the same workspace. It does not check that the earlier job ran on your deployment, and it sends no file or handle.
- Keep a map from each job id to your own handle. If you do not know the id, send a `failed` event. The job costs nothing and the exchange tries the next offer.
- Each job is charged for its own seconds.

## Cancel and expiry

The exchange sends no cancel message. It closes the dispatch connection in these cases:

- The buyer calls `POST /v1/videos/{id}/cancel`. The exchange closes the connection within a few seconds. The job costs nothing.
- The job runs past the video job limit, 10 minutes by default. The buyer sees `expired`. The job costs nothing, and it counts as a failure of your deployment.

Watch for the close. When it comes, stop the work on your upstream if your upstream has a way to cancel. If it has none, you carry the cost of the render.

## Start the agent

1. Set `LIQUID_UPSTREAM_BASE_URL` and the key of your upstream.
2. Set `LIQUID_CONTENT_KEY` to a stable secret. The agent signs the download token with it. Keep the same value after a restart. If the value changes, the buyer cannot download a video that is already paid for.
3. Start the agent and wait for `listed`, then quote the deployment. See [Run your agent](https://inference.ai.exchange/docs/provider-operate.md).
4. Keep the agent running for as long as your files stay on the upstream. The buyer downloads through the agent.

## Limits and failures

- The exchange limits the whole job, by default to ten minutes. The agent gives up before that limit and reports its own reason.
- If the exchange closes the dispatch, the agent stops to wait. Your upstream keeps rendering, because no cancel route works on every gateway. You carry the cost of that render. See [Cancel and expiry](#cancel-and-expiry).
- The exchange can use the next offer only before the first output. After the agent reports a finished video, the job does not move to another provider.
- These failures end the attempt: a failed job, a wrong status, a file that is not `video/*`, and a missing `id`. The exchange charges nothing and tries the next offer.

## Test before you quote

Run these calls against your upstream. Each one must give the answer that this page describes.

```bash
curl -s $BASE/videos -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"model":"acme-video-1","prompt":"test","seconds":"4"}'
curl -s $BASE/videos/$ID -H "Authorization: Bearer $KEY"
curl -sI $BASE/videos/$ID/content -H "Authorization: Bearer $KEY"
```

Then buy one video from your own deployment with a small price cap, and download it. The buyer's [video guide](https://inference.ai.exchange/docs/video-generation.md) shows the calls.
