---
name: pirateface-zk-inference
description: Private, anonymous LLM inference through the Pirate Face gateway. OpenAI-compatible; personal details are swapped for stand-ins inside a sealed Intel TDX enclave before any model sees them. Use when the user wants their agent's model calls to go through ZK Inference.
homepage: https://pirateface.co/inference
---

# ZK Inference (Pirate Face)

ZK Inference is an OpenAI-compatible API for hundreds of models (OpenRouter's catalog). The user paid for their key with a zero-knowledge proof, so nobody can tie the requests to them. The gateway runs in an Intel TDX enclave: it swaps names, emails, phone numbers and other personal details for realistic stand-ins before OpenRouter sees a request, and puts the real ones back in the answer.

## Files

| What | URL |
|------|-----|
| This file | `https://pirateface.co/zk-inference.md` |
| API base (OpenAI-compatible) | `https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1` |
| The user's setup page (Copy key) | `https://pirateface.co/inference` |
| Add funds | `https://pirateface.co/settings#balance` |

## The key

The key starts with `pf-gw-`. Read it from the environment variable `PF_ZK_KEY`.

- If `PF_ZK_KEY` is not set, the user may not have a key yet: a key is created the first time they add funds. Walk them through it, one step at a time:
  1. Log in on https://pirateface.co (Log in, top right), then add funds at https://pirateface.co/settings#balance: connect the wallet, pick an amount ($2 minimum) and pay in USDC on Base. The money becomes credit on their key.
  2. On https://pirateface.co/inference, press **Copy key** (it's there once they've added funds, in the same browser).
  3. Make it `PF_ZK_KEY` in the environment you start in, then restart you (a running process doesn't see new variables). Best is a key file, which keeps it out of shell history: save the key to `~/.config/pirateface/zk-key` (`chmod 600`) and have the startup run `export PF_ZK_KEY="$(cat ~/.config/pirateface/zk-key)"`. Or, for one shell: `export PF_ZK_KEY=<the key>`.
- Why this stays anonymous: the USDC goes into a shared vault on Base, and the user's browser moves it onto the key with a zero-knowledge proof, so nobody can link the key to the wallet or the account that paid. The key is kept in that browser's local storage, which is why adding funds from another browser makes a new key.
- Never print, log, echo or commit the key. Never paste it into a chat, an issue or a prompt. Reference it through the environment variable.
- The balance belongs to the key. Anyone with the key can spend it.

## Start

Do this for the user.

1. Check the key and the balance:

   ```bash
   curl -s https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1/credit -H "Authorization: Bearer $PF_ZK_KEY"
   ```

   It answers `{"credit_usd": …, "spent_usd": …}`. If `credit_usd` is below 0.01, the key has no money left: ask the user to add funds at https://pirateface.co/settings#balance (log in, connect a wallet, pay in USDC on Base; $2 minimum) and wait until they say it's done. Added from the same browser, the credit lands on the same key: nothing to change. From another browser it makes a new key, which then has to replace `PF_ZK_KEY`.

2. Point the harness you run in at the gateway. Use the first that fits:

   - **Codex.** Add this to `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`; merge, keep what's there). It adds a profile, so the user's default setup stays as it is:

     ```toml
     [model_providers.pirateface]
     name = "Pirate Face ZK Inference"
     base_url = "https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1"
     env_key = "PF_ZK_KEY"
     wire_api = "responses"

     [profiles.pirateface]
     model_provider = "pirateface"
     model = "deepseek/deepseek-v4.1-flash-1017"
     ```

     Codex reads the key from `PF_ZK_KEY` when it starts, so the key never sits in the file. The user starts it with `codex --profile pirateface` (a running Codex keeps its old settings until restarted).

     **Web search.** Codex's built-in web search runs on OpenAI's servers, so it isn't available through the gateway: the gateway skips it, as it skips all of OpenAI's built-in tools. Give Codex a search tool of its own instead. Tell the user first: searches go from their computer straight to the search service, with the real details in them (the gateway puts those back before Codex runs a tool), not through the enclave. If they agree, add Exa's search server (an MCP server: a standard way to plug tools into agents). It works without an account:

     ```toml
     [mcp_servers.exa]
     url = "https://mcp.exa.ai/mcp"
     ```

     Skip this if the config already has a search MCP server. After the restart, check it's there with `codex mcp list`.
   - **OpenAI-compatible clients and SDKs.** Base URL `https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1`, API key `$PF_ZK_KEY`. Many tools read `OPENAI_BASE_URL` and `OPENAI_API_KEY`; set those only for this tool, not globally.
   - **Pi.** Add a provider to `~/.pi/agent/models.json` (merge, keep existing providers):

     ```json
     {
       "providers": {
         "pirateface": {
           "baseUrl": "https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1",
           "api": "openai-completions",
           "apiKey": "$PF_ZK_KEY",
           "compat": { "supportsDeveloperRole": false, "supportsUsageInStreaming": true, "maxTokensField": "max_tokens", "thinkingFormat": "openrouter" },
           "models": [{ "id": "deepseek/deepseek-v4.1-flash-1017", "name": "DeepSeek V4.1 Flash 1017 (ZK Inference)", "reasoning": true, "input": ["text"], "contextWindow": 1048576, "maxTokens": 32768 }]
         }
       }
     }
     ```

     Pi fills in `$PF_ZK_KEY` from the environment, so the key never sits in the file. Then use `pi --model pirateface/deepseek/deepseek-v4.1-flash-1017`.
   - **Python (openai package).**

     ```python
     import os
     from openai import OpenAI
     client = OpenAI(base_url="https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1", api_key=os.environ["PF_ZK_KEY"])
     ```

3. Send one short test request and tell the user it works:

   ```bash
   curl -s https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1/chat/completions -H "Authorization: Bearer $PF_ZK_KEY" -H "Content-Type: application/json" \
     -d '{"model":"deepseek/deepseek-v4.1-flash-1017","messages":[{"role":"user","content":"Reply with: ZK Inference works"}]}'
   ```

## Use

- **Models.** Suggest `deepseek/deepseek-v4.1-flash-1017` (DeepSeek V4.1 Flash 1017): cheap, fast, 1M context, and good at agent work with tools. Pirate Face runs its own agents on it. `GET https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-3402.dstack-pha-prod5.phala.network/gw/v1/models` lists every other model (OpenRouter ids); use one only if the user asks for it.
- **Requests.** `POST /chat/completions` or `POST /responses`, OpenAI format. Streaming, tools and reasoning work as on OpenRouter. With `/responses`, send the whole conversation every turn (`store: false`): the gateway keeps nothing, so `previous_response_id` is refused. Only your own function tools work; OpenAI's built-in tools (`web_search`, `file_search`, `code_interpreter`...) are skipped.
- **Cost.** `usage.cost` in each answer is what the key paid in USD: the model, the scrubbing, and Pirate Face's 20% margin.
- **Low balance.** `max_tokens` is cut to what the balance can pay for, so a long answer can stop early (`finish_reason: "length"`). If that happens on a low balance, tell the user to add funds.
- **Keep the user ahead of it.** Check `/credit` now and then (not before every request). When `credit_usd` drops below $0.50, say so once and point to https://pirateface.co/settings#balance, so they top up before work stops.
- **What left the enclave.** Add `"pf_sent": true` to a request to get the last user message as OpenRouter received it (stand-ins in place), as `pf_sent` in the JSON answer, or as a first SSE comment `: pf-sent "<text>"` when streaming.
- **Write naturally.** Send the user's text as it is. The gateway hides personal details itself; don't anonymize by hand.
- **Secrets are not personal details.** Passwords, API keys and private keys are not reliably hidden. Never send them.
- **Names in code.** The scrubber can also swap a name that is really an identifier in code. If an answer looks off, send the request again with `pf_sent` and look at what the model received.

## Verify

What protects the user is the code running in the enclave, and anyone can read it. The gateway's whole code is inline in its Docker compose, and the enclave publishes that compose in its attestation:

```bash
curl -s "https://4564fa51b494e1cc2234b1bc71eb31ff18d10eb5-8090.dstack-pha-prod5.phala.network/prpc/Info?json" | python3 -c "import json,sys,hashlib; d=json.load(sys.stdin); t=json.loads(d['tcb_info']); c=t['app_compose']; print('compose hash matches:', hashlib.sha256(c.encode()).hexdigest()==d['compose_hash']); print(c[:400])"
```

`compose_hash` is measured into the TDX quote (Phala dstack), so the code printed is the code that runs. Read it if the user asks how their details are handled. "ZK" is the payment: the proof that hides who paid. The enclave is what hides the details.

## Errors

| Status | Meaning | Do |
|--------|---------|----|
| 401 | Unknown key | Ask the user to press Copy key on https://pirateface.co/inference and set `PF_ZK_KEY` again (in the environment, not in the chat). |
| 402 `insufficient_quota` | Balance too low for this request | Tell the user to add funds at https://pirateface.co/settings#balance. Don't retry. |
| 400 `model not available` | Not in the catalog | Pick a model from `/models`. |
| 400 `previous_response_id is not supported` | `/responses` with stored state | Send the whole conversation in `input` with `store: false`. |
| 503 | The scrubber or the catalog is briefly unavailable | Retry once after 30 s, then tell the user. |

## Leaks

If a personal detail reaches the answer unchanged when it should have been hidden, tell the user. They can report it to @thepirateface on X. Describe the kind of detail ("a street name"); don't repeat the detail itself.
