docs: supplier deploy runbook for self-hosted operators
Generic runbook for deploying a mainnet supplier on any operator-owned server: own DNS/TLS via the in-repo Traefik compose, docker-run CLI forms (no host Node), advert posted last as the go-live flag, verification via the public indexer. Fleet-specific steps stay in HETZNER_SETUP.md.
This commit is contained in:
291
docs/DEPLOY_NEW_SUPPLIER.md
Normal file
291
docs/DEPLOY_NEW_SUPPLIER.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# Deploy a New Supplier — Operator Runbook
|
||||||
|
|
||||||
|
> Audience: anyone who wants to sell inference on the agents-marketplace by
|
||||||
|
> running a **mainnet supplier** on a server they control. Everything below
|
||||||
|
> uses your own hardware, DNS, and funds — no access to the Apex Fusion fleet
|
||||||
|
> is required. (Fleet-internal runbooks: `docs/HETZNER_SETUP.md`,
|
||||||
|
> `deploy/README.md`. A live worked example of exactly this standalone
|
||||||
|
> pattern: `deploy/inference-proxy/README.md`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What a supplier is
|
||||||
|
|
||||||
|
A supplier is one Docker container that:
|
||||||
|
|
||||||
|
1. Holds an **on-chain advertisement** (a UTxO on the Vector mainnet chain)
|
||||||
|
declaring capability, model, price, bonds, and your endpoint URL.
|
||||||
|
2. Serves a **public HTTPS endpoint** that buyers call after locking payment
|
||||||
|
in escrow. Any hostname you control works — the advert carries the URL,
|
||||||
|
and the marketplace indexer probes it as-is.
|
||||||
|
3. Runs the job against an upstream inference backend and settles on-chain
|
||||||
|
(Claim → Submit; bonds of 1 AP3X ride on each job).
|
||||||
|
|
||||||
|
One supplier = one (model × capability) pair = one wallet = one advert = one
|
||||||
|
compose file. Capabilities: `llm.text.generate.v1` (one-off) or `llm.chat.v1`
|
||||||
|
(multi-turn session, `CAPABILITY_KIND=chat-session`). There is also a TTS
|
||||||
|
kind (`CAPABILITY_KIND=tts`); this runbook covers the LLM kinds.
|
||||||
|
|
||||||
|
## 2. Prerequisites
|
||||||
|
|
||||||
|
| Requirement | Notes |
|
||||||
|
|---|---|
|
||||||
|
| Linux server | Public IPv4, Docker installed, ports 80 + 443 free (for the bundled Traefik). A small VPS is enough — the container only proxies and settles; the heavy compute is upstream. |
|
||||||
|
| DNS name you control | One A record, e.g. `supplier.example.com` → your server IP. Must be **DNS-only / unproxied** — TLS certs come from Let's Encrypt HTTP-01, which a CDN-proxied record breaks. |
|
||||||
|
| Upstream backend | Any OpenAI-compatible API: base URL + API key. Proven in production: hosted APIs (DeepSeek, OpenRouter, Hetzner Inference), local llama.cpp, agent gateways. |
|
||||||
|
| Model id | Must be the **verbatim** upstream id (incl. prefixes like `Qwen/`). Verify against the backend's `/v1/models`. |
|
||||||
|
| Capability | `llm.text.generate.v1` or `llm.chat.v1` (or both → two suppliers, two wallets). |
|
||||||
|
| Advert params | Your call. Reference values used by the Apex Fusion fleet: price `200000` lovelace (0.2 AP3X), bonds `1000000` both sides, `max_processing_ms` `300000` one-off / `1800000` chat, `max_output_tokens` = model context length. |
|
||||||
|
| Funding | ≥50 AP3X mainnet to the new supplier wallet, from your own funds (the fleet typically funds 50–200). Each advert locks a 1 AP3X supplier bond; each job rides another. |
|
||||||
|
|
||||||
|
## 3. Vector mainnet constants (same for every operator)
|
||||||
|
|
||||||
|
These are chain-level values, identical for all suppliers; put them in every
|
||||||
|
supplier env file:
|
||||||
|
|
||||||
|
```
|
||||||
|
NETWORK_ID=1
|
||||||
|
LIVE_CHAIN=1
|
||||||
|
OGMIOS_URL=https://ogmios.vector.mainnet.apexfusion.org
|
||||||
|
VECTOR_ZERO_TIME_MS=1756485600000
|
||||||
|
ESCROW_REF_UTXO=c8d84c6d67ec67a1efe5e9c6c06d53020e05d1bb96d1c55ecb1eb7d5010c4d54#0
|
||||||
|
ADVERT_REF_UTXO=c8d84c6d67ec67a1efe5e9c6c06d53020e05d1bb96d1c55ecb1eb7d5010c4d54#1
|
||||||
|
```
|
||||||
|
|
||||||
|
`OGMIOS_URL` above is a public endpoint; if you run your own Vector node +
|
||||||
|
Ogmios, point at that instead. Never pin `NETWORK_ID` / `OGMIOS_URL` inside
|
||||||
|
the compose file — they must come from the env file (prevents
|
||||||
|
mainnet/testnet cross-wiring on re-up).
|
||||||
|
|
||||||
|
## 4. Procedure
|
||||||
|
|
||||||
|
### 4.1 Smoke-test the backend first (before spending on-chain)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -X POST <BASE_URL>/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"<MODEL_ID>","messages":[{"role":"user","content":"ping"}]}' \
|
||||||
|
| jq '.choices[0].message.content'
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements: non-empty string content, **no `max_tokens` sent** (matches
|
||||||
|
runtime behavior). For a chat supplier, also verify a `tools`/`tool_choice`
|
||||||
|
round-trip. A backend that fails these costs you the 1 AP3X supplier bond per
|
||||||
|
failed job (`openai_malformed`).
|
||||||
|
|
||||||
|
### 4.2 Prepare the host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com | sh # if Docker is missing
|
||||||
|
git clone https://github.com/Apex-Fusion/agents-marketplace.git
|
||||||
|
cd agents-marketplace
|
||||||
|
docker network create traefik-net # one-shot
|
||||||
|
|
||||||
|
# TLS terminator (Traefik + Let's Encrypt). One per host; serves every
|
||||||
|
# supplier you run on it.
|
||||||
|
cd deploy/inference-proxy
|
||||||
|
echo 'ACME_EMAIL=<your email>' > .env && chmod 600 .env
|
||||||
|
docker compose -f docker-compose.traefik.yml up -d
|
||||||
|
cd ../..
|
||||||
|
```
|
||||||
|
|
||||||
|
Already have your own reverse proxy? Skip Traefik: terminate TLS for
|
||||||
|
`https://<your-dns-name>` yourself and forward to the supplier container's
|
||||||
|
port 8080, then drop the `traefik` labels and network from the compose file
|
||||||
|
in §4.4.
|
||||||
|
|
||||||
|
### 4.3 DNS
|
||||||
|
|
||||||
|
Create `<your-dns-name>` → A record for your server IP, **DNS-only /
|
||||||
|
unproxied**. Traefik issues the Let's Encrypt cert via HTTP-01 on port 80
|
||||||
|
automatically ~1 min after propagation; no restart needed.
|
||||||
|
|
||||||
|
### 4.4 Compose file
|
||||||
|
|
||||||
|
Copy the standalone-box template and rename its identity strings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p deploy/self-hosted
|
||||||
|
cp deploy/inference-proxy/docker-compose.supplier-local.yml \
|
||||||
|
deploy/self-hosted/docker-compose.supplier-<name>.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
(The relative paths inside — `build.context: ../..`,
|
||||||
|
`env_file: ../../supplier/.env.<name>` — assume the file sits two directory
|
||||||
|
levels below the repo root.)
|
||||||
|
|
||||||
|
Rename consistently:
|
||||||
|
|
||||||
|
- `name:` and `container_name` → `marketplace-mainnet-supplier-<name>`
|
||||||
|
- `env_file` path → `../../supplier/.env.<name>`
|
||||||
|
- every Traefik label token `mp-suppliers-local-mainnet` → a token of your
|
||||||
|
own (router names must be unique per Traefik instance)
|
||||||
|
- `Host(...)` rules → your DNS name
|
||||||
|
- `OPENAI_BASE_URL` → your backend, **without `/v1`** (the client appends
|
||||||
|
`/v1/chat/completions`)
|
||||||
|
- `OPENAI_TIMEOUT_MS` ≤ the advert's `max_processing_ms`
|
||||||
|
|
||||||
|
Capability kind:
|
||||||
|
|
||||||
|
- **One-off** (`llm.text.generate.v1`): delete the chat block —
|
||||||
|
`CAPABILITY_KIND`, `CHAT_IDLE_TIMEOUT_MS`, `CHAT_SETTLE_MODE`,
|
||||||
|
`MAX_CHAT_SESSIONS`.
|
||||||
|
- **Chat** (`llm.chat.v1`): keep `CAPABILITY_KIND: chat-session`. Delete
|
||||||
|
`CHAT_SETTLE_MODE: ticket` unless you know you want it — unset means full
|
||||||
|
settle (Claim/Submit, i.e. you collect payment on-chain); `ticket` skips
|
||||||
|
all supplier chain ops and the buyer reclaims the escrow.
|
||||||
|
|
||||||
|
The mainnet fleet templates (`deploy/mainnet/docker-compose.supplier-*.yml`)
|
||||||
|
are also valid references, but they attach to `marketplace-mainnet-net`,
|
||||||
|
which does not exist on a standalone box — remove it if you start from one
|
||||||
|
of those.
|
||||||
|
|
||||||
|
Compose files are safe to commit to your fork (no secrets); env files are
|
||||||
|
never committed.
|
||||||
|
|
||||||
|
### 4.5 Generate and fund the wallet
|
||||||
|
|
||||||
|
No Node needed on the host — build the supplier image once and run the CLI
|
||||||
|
inside it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/self-hosted/docker-compose.supplier-<name>.yml build
|
||||||
|
docker compose -f deploy/self-hosted/docker-compose.supplier-<name>.yml \
|
||||||
|
run --rm --no-deps supplier \
|
||||||
|
node_modules/.bin/tsx supplier/src/cli/gen-keypair.ts --network 1
|
||||||
|
# prints privateKeyHex, publicKeyHex, pubKeyHash, address
|
||||||
|
```
|
||||||
|
|
||||||
|
(With Node + pnpm available locally:
|
||||||
|
`pnpm --filter @marketplace/supplier tx:gen-keypair --network 1`.)
|
||||||
|
|
||||||
|
Fund the printed address from your own wallet (≥50 AP3X). Optimal UTxO shape
|
||||||
|
is **2 UTxOs**: one ≥5 AP3X collateral + one working UTxO. If the wallet ends
|
||||||
|
up fragmented later, run `tx:consolidate-wallet` (can be docker-exec'd inside
|
||||||
|
the running supplier container — it already has the key via env).
|
||||||
|
|
||||||
|
### 4.6 Env file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp supplier/.env.example supplier/.env.<name>
|
||||||
|
chmod 600 supplier/.env.<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Must contain, at minimum:
|
||||||
|
|
||||||
|
- **All four wallet vars**: `SUPPLIER_PRIV_KEY_HEX`, `SUPPLIER_ADDRESS`,
|
||||||
|
`SUPPLIER_PKH`, `SUPPLIER_PUB_KEY_HEX`. They are NOT derived at boot;
|
||||||
|
a missing derived var causes `403 wrong_supplier` on every job.
|
||||||
|
- `OPENAI_API_KEY` (empty is legal only for unauthenticated backends).
|
||||||
|
- The shared plumbing block from §3.
|
||||||
|
- `ADVERT_REF=` left as placeholder until §4.8.
|
||||||
|
|
||||||
|
Backend selection (`LLM_BACKEND=openai`, `OPENAI_BASE_URL`,
|
||||||
|
`OPENAI_TIMEOUT_MS`, `CAPABILITY_KIND`) lives in the compose file (§4.4),
|
||||||
|
not the env file.
|
||||||
|
|
||||||
|
### 4.7 First start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/self-hosted/docker-compose.supplier-<name>.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify before going on-chain:
|
||||||
|
|
||||||
|
- `docker logs marketplace-mainnet-supplier-<name> --tail 50` — no
|
||||||
|
crash-loop.
|
||||||
|
- `curl https://<your-dns-name>/healthz` → `{"ok":true}` (proves DNS, cert
|
||||||
|
issuance, and routing end-to-end).
|
||||||
|
|
||||||
|
Every deploy on your box is manual: `git pull` +
|
||||||
|
`docker compose ... up -d --build`. No CD pipeline covers it.
|
||||||
|
|
||||||
|
### 4.8 Post the advert (go-live — do this LAST)
|
||||||
|
|
||||||
|
The advert is the go-live flag: the moment it confirms, the indexer starts
|
||||||
|
probing `<endpoint>/status` and buyers can lock escrows. Post it only after
|
||||||
|
§4.7 is green, so buyers never hit a dead endpoint.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/self-hosted/docker-compose.supplier-<name>.yml \
|
||||||
|
run --rm --no-deps supplier \
|
||||||
|
node_modules/.bin/tsx supplier/src/cli/post-advert.ts \
|
||||||
|
--capability-id llm.text.generate.v1 \
|
||||||
|
--model '<MODEL_ID>' \
|
||||||
|
--max-output-tokens <CTX_LEN> \
|
||||||
|
--max-processing-ms 300000 \
|
||||||
|
--price-lovelace 200000 \
|
||||||
|
--endpoint-url https://<your-dns-name>
|
||||||
|
# chat variant: --capability-id llm.chat.v1 --max-processing-ms 1800000
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste the printed `<txHash>#0` into `ADVERT_REF` in `supplier/.env.<name>`,
|
||||||
|
then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/self-hosted/docker-compose.supplier-<name>.yml up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: adverts are immutable — changing the model, price, or endpoint later
|
||||||
|
requires `tx:retire-advert` (refunds the advert bond) + a fresh post-advert
|
||||||
|
+ a new `ADVERT_REF`.
|
||||||
|
|
||||||
|
## 5. Verification checklist
|
||||||
|
|
||||||
|
- `curl https://<your-dns-name>/healthz` → `{"ok":true}`.
|
||||||
|
- `/capability` on the same host shows the right model + pkh.
|
||||||
|
- Public marketplace indexer lists your advert:
|
||||||
|
`curl https://mp-indexer.vector.apexfusion.org/suppliers` → your entry with
|
||||||
|
`advert_status: "Active"`, `status: "free"`. The indexer follows the chain,
|
||||||
|
so a confirmed advert appears automatically — no registration step.
|
||||||
|
(The buyer API and OpenAI gateway at `marketplace.vector.apexfusion.org` /
|
||||||
|
`api.marketplace.vector.apexfusion.org` require buyer API keys; buyers with
|
||||||
|
keys will see your model in the gateway's `/openai/v1/models`.)
|
||||||
|
- Tail `docker logs -f marketplace-mainnet-supplier-<name>` through the
|
||||||
|
first paid jobs; watch for `openai_malformed` (reasoning-model
|
||||||
|
content-shape risk) and `403 wrong_supplier` (missing derived wallet vars).
|
||||||
|
|
||||||
|
## 6. Ongoing operations
|
||||||
|
|
||||||
|
- **Update**: `git pull && docker compose -f ... up -d --build`.
|
||||||
|
- **Model swap**: `tx:retire-advert` → post-advert with the new `--model` →
|
||||||
|
new `ADVERT_REF` in the env file → `up -d --force-recreate`.
|
||||||
|
- **Wallet health**: keep the 2-UTxO shape; run `tx:consolidate-wallet` if
|
||||||
|
script-spends start failing with collateral-selector errors.
|
||||||
|
- **Exit**: `tx:retire-advert` refunds the advert bond and delists you; the
|
||||||
|
wallet keeps its funds.
|
||||||
|
- **Monitoring (optional)**: the repo ships a balance monitor
|
||||||
|
(`wallet-monitor/`, `buyer/scripts/monitor-wallets.ts`) you can run against
|
||||||
|
your own wallet list.
|
||||||
|
|
||||||
|
## 7. Footguns (each has burned an operator before)
|
||||||
|
|
||||||
|
1. `OPENAI_BASE_URL` must NOT end in `/v1`.
|
||||||
|
2. Advert `--model` must match the upstream id byte-for-byte; slashes are
|
||||||
|
fine on-chain.
|
||||||
|
3. Do not set `OPENAI_REASONING` unless the backend is OpenRouter.
|
||||||
|
4. `OPENAI_TIMEOUT_MS` > advert `max_processing_ms` = bond-forfeit window.
|
||||||
|
5. Model ids containing `kimi`, `deepseek`, or `gpt` (case-insensitive) are
|
||||||
|
auto-enrolled in the marketplace buyer's PDF-summarizer pool
|
||||||
|
(`buyer/src/pdf/caps.ts`), so expect PDF jobs too. The
|
||||||
|
`PDF_MODEL_ALLOWLIST` / `PDF_MODEL_DENYLIST` overrides are buyer-side —
|
||||||
|
only relevant if you also operate your own buyer.
|
||||||
|
6. `LIVE_CHAIN` accepts only the literal `"1"`; `true`/`yes` silently means
|
||||||
|
off, and the supplier then never broadcasts.
|
||||||
|
7. The compose `env_file` uses `required: false`, so a typo'd env path comes
|
||||||
|
up and crash-loops instead of failing to parse — check logs on first boot.
|
||||||
|
8. Wallet fragmentation (>2 UTxOs, none ≥5 AP3X) breaks all script-spends
|
||||||
|
with a lucid collateral selector error; fix with `tx:consolidate-wallet`.
|
||||||
|
|
||||||
|
## 8. Key references in this repo
|
||||||
|
|
||||||
|
- `deploy/inference-proxy/README.md` — a live standalone box built exactly
|
||||||
|
this way (two suppliers, incl. a tailnet-backed home rig).
|
||||||
|
- `deploy/README.md` — networks, Traefik, per-service ops.
|
||||||
|
- `docs/HETZNER_SETUP.md` — the Apex Fusion fleet runbook (hosted-API
|
||||||
|
suppliers at scale).
|
||||||
|
- `docs/HUGGINGFACE_ROUTER_SETUP.md` — OpenRouter/HF-router backend variant.
|
||||||
|
- `supplier/.env.example` — every env var, annotated (plus `.env.*.example`
|
||||||
|
variants per backend).
|
||||||
|
- `supplier/src/cli/` — `gen-keypair`, `post-advert`, `retire-advert`,
|
||||||
|
`consolidate-wallet`, `publish-reference-scripts`.
|
||||||
Reference in New Issue
Block a user