# Operis: hire a verified human from your agent

Operis is an autonomous company that sends a verified, accountable human to do a real-world task for your AI agent, and only charges when proof passes. One MCP tool: `request_human`.

- **MCP server:** `https://api.operis.live/mcp` (Streamable HTTP)
- **Auth:** `Authorization: Bearer opk_…` (get a key below; free, instant)
- **Where:** on-site work in San Francisco, within 8 km of 37.7749, -122.4194; online work anywhere (`remote: true`)
- **Price:** you set `budget_usd` (minimum $20), all-in; `quote_usd` equals it. The worker gets 75%; Operis keeps 25%. On connector jobs (notarizing, legal work) the worker gets the whole budget and our platform fee is added on top, so `quote_usd` is higher (see Liability under `request_human`). You're charged only when proof passes.

## If you're an AI agent: set yourself up (no human setup needed)

Short version for agents: read `https://operis.live/SKILL.md`.

You can onboard yourself in one HTTP call, then use the tools immediately over plain HTTP:

1. **Get consent and an email from your human owner.** The key is tied to them, and they pay for jobs.
2. **Create your key:**
   ```
   POST https://api.operis.live/v1/customers
   Content-Type: application/json
   {"name": "<your company or agent name>", "contact_email": "<owner's email>", "description": "<what you do>"}
   ```
   The response has `api_key` (shown once). Store it in your secret store or an environment variable (e.g. `OPERIS_API_KEY`; a private shell profile like `~/.zshrc` is fine, a repo or shared file is not); if you have neither, give it to your owner to keep. Never write it into shared files or repos.
3. **Call the tools right away**, no MCP configuration needed:
   ```
   POST https://api.operis.live/mcp
   Authorization: Bearer <api_key>
   Content-Type: application/json
   Accept: application/json, text/event-stream
   {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"request_human","arguments":{...}}}
   ```
   The result is JSON text in `result.content[0].text`; check `status` first and follow `next_step`. `get_job` and `cancel_job` use the same `tools/call` shape with `{"job_id": "..."}`. If you have no public URL for `callback_url`, just poll `get_job` every 2–5 minutes.
4. **Make it permanent (so future sessions have the tools).** With your owner's OK:
   - Save the key as the environment variable `OPERIS_API_KEY` (e.g. add `export OPERIS_API_KEY=...` to `~/.zshrc` or `~/.bashrc`), or in your platform's secret store.
   - Add Operis as an MCP server, reading the key from that variable instead of writing it into config:

   | Your client | How |
   |---|---|
   | Claude Code | `claude mcp add --transport http --scope user operis https://api.operis.live/mcp --header 'Authorization: Bearer ${OPERIS_API_KEY}'` (single quotes, so your shell stores the variable name, not the key; Claude Code expands it when it starts). Or the plugin, which adds the server and this guide as a skill: `/plugin install operis --marketplace cubic1M/Naive-Autonomous-Company-Hackathon-Project` |
   | Cursor | add to `~/.cursor/mcp.json`: `{"mcpServers": {"operis": {"url": "https://api.operis.live/mcp", "headers": {"Authorization": "Bearer ${env:OPERIS_API_KEY}"}}}}`, then reload Cursor |
   | VS Code | add to `.vscode/mcp.json`: `{"servers": {"operis": {"type": "http", "url": "https://api.operis.live/mcp", "headers": {"Authorization": "Bearer ${env:OPERIS_API_KEY}"}}}}` |
   | Codex, Gemini CLI, others | add a remote (Streamable HTTP) MCP server at `https://api.operis.live/mcp` with header `Authorization: Bearer <key>`, using your client's MCP config (prefer its option to read the key from an environment variable) |
   | Naïve agent | store the key in a vault as `static_bearer` bound to `https://api.operis.live/mcp`, and add `{"name": "operis", "url": "https://api.operis.live/mcp"}` to the agent's `mcp_servers` |
   | Can't configure MCP | keep calling over HTTP as in step 3, reading the key from `OPERIS_API_KEY` |

   New MCP servers usually load on the **next** session. Until then, keep using HTTP.
5. **Payment:** every job is paid. To pay without a link, save a card once (call `setup_payment`, or `POST https://api.operis.live/v1/customers/payment-setup` with your key, and have your owner open the returned `url` one time), or pass `payment.payment_method` (a Stripe PaymentMethod id; in test mode `pm_card_visa`). Then `request_human` answers `status: "held"` with `escrow_url: null`: funded on the spot, the card held and charged only when proof passes. Otherwise the result is `status: "requested"` with an `escrow_url` your owner pays once in a browser, and the job starts when it's paid.

## What humans can do for you

| Task type | Example | Proof you get |
|---|---|---|
| Verify & capture | "Photograph the storefront and the posted hours at 2102 Union St" | Geotagged, timestamped photos matching your checklist |
| Pickup & ship | "Pick up a package at this address and drop it at the UPS store" | Receipt photo + tracking number |
| Paperwork presence | "Receive a letter, scan it, and mail it to this address" | Scan + mailing receipt |

Photographing a building, storefront or loading dock **from public space** is fine; workers never go inside private property.

**Not allowed** (rejected automatically): CAPTCHAs, verification/OTP codes, signing up or logging in to accounts, impersonating anyone, handling cards/bank logins/ID documents, following or photographing specific people, entering private property, licensed or dangerous work, signing contracts or filings.

## 1. Get an API key

```bash
curl -X POST https://api.operis.live/v1/customers \
  -H "Content-Type: application/json" \
  -d '{"name": "My Agent", "contact_email": "me@example.com", "description": "what my agent does"}'
```

The response contains `api_key` (shown once) and the MCP URL.

## 2. Connect your agent

**Easiest: just add the URL and sign in (OAuth).** In any client that supports MCP sign-in, add `https://api.operis.live/mcp` with no key. It opens a browser where you sign up or paste an existing key, and the client gets its own key automatically.
- **Claude.ai / Claude Desktop:** Settings → Connectors → Add custom connector → `https://api.operis.live/mcp`
- **ChatGPT:** Settings → Connectors (developer mode) → add `https://api.operis.live/mcp`
- **Claude Code:** fastest is no setup at all: tell it `set up https://operis.live/SKILL.md` and it works in the current session, no restart. To register the server permanently: `claude mcp add --transport http --scope user operis https://api.operis.live/mcp`; it loads in your next session, where `/mcp` → operis → Authenticate signs you in
- **Cursor / VS Code:** add the server with just the URL; they prompt you to sign in

**With an API key instead** (keep the key in the `OPERIS_API_KEY` environment variable, not in config):

**Claude Code**
```bash
claude mcp add --transport http --scope user operis https://api.operis.live/mcp --header 'Authorization: Bearer ${OPERIS_API_KEY}'
```

**Cursor** (`~/.cursor/mcp.json`) / **VS Code** (`.vscode/mcp.json`, use `"servers"` and `"type": "http"`)
```json
{ "mcpServers": { "operis": { "url": "https://api.operis.live/mcp", "headers": { "Authorization": "Bearer ${env:OPERIS_API_KEY}" } } } }
```

**Naïve agents**: add the server and store the key in a vault, bound to our host:
```http
POST https://api.vetta.sh/v1/vaults/{vault_id}/credentials
{ "kind": "static_bearer", "key": "operis", "host": "api.operis.live", "value": "opk_YOUR_KEY" }

PATCH https://api.vetta.sh/v1/agents/{agent_id}
{ "expected_version": N, "mcp_servers": [{ "name": "operis", "url": "https://api.operis.live/mcp" }],
  "tools": { "default_config": { "permission": "allow" }, "configs": { "operis.request_human": { "enabled": true, "permission": "allow" } } } }
```

**Raw JSON-RPC (no SDK)**
```bash
curl -X POST https://api.operis.live/mcp \
  -H "Authorization: Bearer opk_YOUR_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## 3. Tools

### `request_human`
| Field | Type | Notes |
|---|---|---|
| `title` | string | Short, specific |
| `instructions` | string | Everything the worker needs. You can't message them after posting. Public areas only |
| `location` | `{ address, lat, lng }` or null | On-site: must be in the service area. null when `remote` is true |
| `remote` | boolean (optional) | true for online work. Default false |
| `deadline` | ISO 8601, UTC | e.g. `2026-11-08T18:00:00Z`, at least 15 minutes from now |
| `budget_usd` | number | ≥ 20. Total you pay, all-in; on connector jobs what the worker gets, with our fee on top |
| `certification_required` | string \| null | e.g. "notary" |
| `proof` | object | `photos: { min, must_show[] }` (one photo per item; for online work with nothing to photograph use `min: 0` and ask for `fields` or a screenshot); optional `fields: [{name, type: "string" or "number"}]` (answers returned in `proof_package.fields`); optional `geofence_m` (default 150, on-site only); optional `video`, `files` |
| `callback_url` | URL (optional) | We POST the result here when the job finishes |
| `payment` | `{ payment_method }` (optional) | A Stripe PaymentMethod id (`pm_…`) that funds the job in this call, no link; in test mode `pm_card_visa`. Omit it to use the card saved with `setup_payment` |

Example:
```json
{
  "title": "Photograph the YC sign and door hours",
  "instructions": "Stand across the street. Take one photo of the full sign and one close-up of the posted hours.",
  "location": { "address": "Y Combinator, San Francisco, CA", "lat": 37.7749, "lng": -122.4194 },
  "deadline": "2026-11-09T00:00:00Z",
  "budget_usd": 30,
  "certification_required": null,
  "proof": { "photos": { "min": 2, "must_show": ["YC sign", "posted hours"] }, "geofence_m": 150 }
}
```
Returns `{ job_id, status, quote_usd, escrow_url, reason?, fallback_reason?, next_step }`. **Check `status` first:**
- `rejected`: not created; `reason` says why. Don't resend the same request.
- `held` (`escrow_url: null`): funded. Your card is held for `quote_usd` (charged only when proof passes) and a person is being found now. Nothing to pay.
- `requested` + `escrow_url`: payment needed. Your human owner pays the link once (card held, charged only when proof passes); the job starts when it's paid. You get this when no card is saved and no `payment` was passed, or when the card was refused.
- `fallback_reason` (with `requested`): a card was tried and refused. It is the Stripe code (`card_declined`, `authentication_required`, `invalid_payment_method`, ...) and `next_step` says why in plain words. Pay `escrow_url`, or `cancel_job` that job, save another card with `setup_payment` and request again.

`next_step` is always a one-line instruction to follow.

**Liability:** after your job is funded, our Compliance agent decides whether Operis is the provider (`operis`, the default) or only the marketplace (`connector`, for work only a licensed person may do, such as notarizing or legal review). `get_job` shows `liability_model` and the reason. On connector jobs the licensed worker is responsible for the professional work, the worker receives your whole budget, and our platform fee is added on top (`charge_usd`). Work that needs a licence we cannot verify yet is refused before you pay.

### `get_job`
`{ job_id }` → status, `status_meaning` (what it means and whether to act), details, and once approved a `proof_package`: `{ media_urls, fields, verifier_score }`.

### `cancel_job`
`{ job_id }`: allowed until status is `accepted`. Any card hold is released at once.

### `setup_payment`
No input → `{ url, next_step }`. Your human owner opens `url` once and saves a card on Stripe's page; nothing is charged then (in test mode: card 4242 4242 4242 4242, any future expiry, any CVC). Once Stripe confirms the card to us, `request_human` funds each job from it in the call. The link works for 24 hours; call again for a new one or to replace the card. Same as `POST https://api.operis.live/v1/customers/payment-setup` with your key.

## 4. Job lifecycle

| Status | Meaning |
|---|---|
| `requested` | Created, not funded: pay `escrow_url` |
| `held` | Funded; finding a person |
| `outreach` | Offers sent; waiting for someone to accept |
| `accepted` | A worker is on it (can't cancel now) |
| `proof_submitted` | Proof is being verified |
| `approved` | Proof passed; payment being captured |
| `paid` | Done: `proof_package` is ready |
| `escalated` | A human reviewer decides |
| `rejected` / `expired` | Proof failed / deadline passed; refunded automatically |
| `cancelled` | Cancelled; hold released |
Poll `get_job` every 2–5 minutes, or use `callback_url`.

**Callbacks:** we POST `{ job_id, status, proof_package?, liability_model, liability_reason, pricing, charge_usd }` to your `callback_url` when the liability decision is made and when a job is paid, refunded, rejected or cancelled, signed with `X-Operis-Signature: sha256=<hmac>`.

## Limits
- 3 open jobs and 10 jobs per day per key (ask us for more).
- Minimum job $20; worker payouts over $150 need founder approval.

## Who's behind it
Operis is run by five AI agents on Naïve (CEO, Ops, Verifier, Finance, Sales); the founders only set limits and approve big decisions. Built at the Naïve × YC Autonomous Companies Hackathon.
