# MySQLBot Verified-Answer & Document Services — API Guide (for AI agents & developers)

This service produces **verified, cited research briefs**, **competitor analyses**, and
**structured data extracted from documents**, fulfilled by a self-hosted LLM behind a
draft → web-verify pipeline. This guide tells an autonomous agent everything needed to use
it programmatically.

## 1. What you need to set up
**An API key.** Three ways to get one — the first is fully automated (no human, no email):

- **Self-provision (prepaid, best for agents):** `POST ?action=register` — no auth required. You get
  back a `sk_...` key immediately (shown once) **with a prepaid balance of 0**. The key is billed
  from that balance: it can place **no** orders until it is funded, so a human just opens the
  returned `top_up_url` once and adds credit (via PayPal). After that the key spends its balance
  automatically, per order, with **no** per-call payment step. See §1a.
- **Self-serve via the portal:** create a client account at
  `https://mysqlbot.com/pages/public/client/login.php`, sign in, open **Team**, and **Create key**.
- **Or email the operator** (see the site's contact page) to be issued an **account-billed** key
  (invoiced by agreement, no prepaid balance).

All calls are plain HTTPS + JSON. Keep the key secret &mdash; it is shown only once and cannot be recovered.

### 1a. Self-provision + prepaid balance (fully automated)
```bash
# 1. Provision a key (no auth). Optionally pass name/email for receipts.
curl -s https://mysqlbot.com/pages/public/serviceApi.php?action=register \
  -H "Content-Type: application/json" -d '{"name":"my agent","email":"you@example.com"}'
# -> {"ok":true,"data":{"api_key":"sk_...","balance":0,"currency":"GBP",
#      "top_up_url":"https://mysqlbot.com/pages/public/serviceTopup.php?key=..&token=..", ...}}
```
- **Fund it:** open `top_up_url` (or hand it to the human paying) and add credit once. Credit lands
  as soon as PayPal confirms.
- **Check the balance any time:** `GET ?action=balance` (with `X-Api-Key`) →
  `{"data":{"billing":"prepaid","balance":<number>,"currency":"GBP","top_up_url":"..."}}`.
- **Spend it:** just call `create_order` as normal. Each order **debits the price from your balance**
  atomically. If the balance is too low the create returns **`402 insufficient_balance`** with
  `data.balance`, `data.required`, and `data.top_up_url` — top up, then retry.
- **Refunds:** if an order fails after all retries, its price is **automatically credited back** to
  your balance (no action needed).

> **Machine discovery:** the API is advertised at `/llms.txt` and described by an OpenAPI 3.0
> spec at `https://mysqlbot.com/openapi.json`; this guide is served as `text/markdown` at
> `https://mysqlbot.com/pages/public/serviceApiGuide.php`.

## 2. Base URL & authentication
- **Base URL:** `https://mysqlbot.com/pages/public/serviceApi.php`
- **Auth:** send your key in the header `X-Api-Key: sk_...` on **every** request except
  `register` (the self-provision call, which needs no auth).
- **Content type:** send `Content-Type: application/json` and a JSON body for POSTs.
- **Rate limit:** per-key, default 20 requests/minute (429 `rate_limited` if exceeded).
- **Envelope:** every response is `{ "ok": <bool>, "error": "<string>", "data": { ... } }`.
  `ok:false` carries a machine-readable `error` code and appropriate HTTP status.

## 3. The flow
1. `create_order` → returns an `order_ref`.
2. Poll `status?ref=<order_ref>` until `status` is `done` (or `failed`).
3. `deliverable?ref=<order_ref>` → returns the finished output.

Fulfilment is asynchronous (a research brief can take a few minutes; extraction is faster).
Poll politely — e.g. every 15–30 seconds.

## 4. Endpoints

### 4.1 Create an order — `POST ?action=create_order`
Body fields:
| field | required | notes |
|-------|----------|-------|
| `service_type` | yes | one of `research_brief`, `competitor_brief`, `extraction` |
| `request` | yes | the research question / competitor set / extraction instructions |
| `source_docs` | for `extraction` | the document TEXT to extract from |
| `source_docs_base64` | alt | base64 of a text file, decoded server-side (use instead of `source_docs`) |
| `title` | no | a short label |
| `priority` | no | `low` \| `standard` (default) \| `high` |
| `deliverable_format` | no | `markdown` (default for briefs) \| `csv` (default for extraction) \| `json` (extraction only — a structured JSON array) \| `text` |
| `idempotency_key` | no | a client-chosen string that makes a retried create safe: the same key returns the **same** order instead of billing again (a timed-out create you resend won't double-charge). Byte-identical content is also auto-deduplicated. |

> **JSON output:** for `extraction`, set `"deliverable_format":"json"` to get the `deliverable` back as a single valid JSON **array of objects** (one per record, your requested fields as keys, JSON `null` for missing values, numbers as JSON numbers) — ready to `JSON.parse`, no CSV parsing. JSON is meaningful for `extraction` only; on a research/competitor brief it is ignored and the report is delivered as `markdown`.

> **Length limit:** `source_docs` (or the decoded `source_docs_base64`) is capped at **300,000 characters**. Over that, the create is **rejected** with `413 source_docs_too_long` (`data.max` = 300000) — never silently truncated, so you are never billed for a clipped document. Split larger inputs across multiple orders.

> **Daily cap:** each key may create up to **200 orders/day**; beyond that, create returns `429 daily_order_cap_reached` (`data.cap`).

Response `data`: `order_ref`, `status` (`pending`), `service_type`, `amount`, `currency`,
`billing`, `note`. An idempotent retry additionally returns `duplicate: true` (the existing
order is returned, **not** re-billed).

### 4.2 Order status — `GET ?action=status&ref=<order_ref>`
Response `data`: `order_ref`, `status` (`pending`\|`processing`\|`done`\|`failed`\|`cancelled`),
`service_type`, `amount`, `currency`, `payment_status` (`paid`\|`refunded`\|…), `tokens`,
`verify_outcome`, `error`, `created_at`, `completed_at`, `deliverable_ready` (bool). A key can
only read its own orders.

### 4.3 Deliverable — `GET ?action=deliverable&ref=<order_ref>`
Returns `409 not_ready` (with `data.status`) until the order is `done`. When done, response
`data`: `order_ref`, `status` (`done`), `service_type`, `deliverable_format`, `deliverable`
(the full output string).

### 4.4 Register — `POST ?action=register` (no auth)
Body (all optional): `name` (a label for the key), `email` (for receipts/notifications). Response
`data`: `api_key` (shown once), `key_prefix`, `billing` (`prepaid`), `balance` (`0`), `currency`,
`top_up_url` (fund the key here), `guide_url`, `note`. Per-IP **and** site-wide hourly caps
(`429 provision_rate_limited`); a fresh key holds no balance.

### 4.5 Balance — `GET ?action=balance`
Response `data`: `billing` (`prepaid` | `account`), `balance` (number), `currency`, `top_up_url`
(for a prepaid key). Account-billed keys report their `billing` with a `0` balance and no top-up URL.

## 5. Examples (curl)

Create a research brief:
```bash
curl -s https://mysqlbot.com/pages/public/serviceApi.php?action=create_order \
  -H "X-Api-Key: sk_YOURKEY" -H "Content-Type: application/json" \
  -d '{"service_type":"research_brief","request":"Summarise the current UK EV grant landscape with sources","deliverable_format":"markdown"}'
# -> {"ok":true,"error":"","data":{"order_ref":"SO-XXXX","status":"pending",...}}
```

Extract data from a document:
```bash
curl -s "https://mysqlbot.com/pages/public/serviceApi.php?action=create_order" \
  -H "X-Api-Key: sk_YOURKEY" -H "Content-Type: application/json" \
  -d '{"service_type":"extraction","request":"Extract each invoice as columns invoice_no, company, amount, date","deliverable_format":"csv","source_docs":"<your document text here>"}'
```

Extract data as JSON (for code / APIs):
```bash
curl -s "https://mysqlbot.com/pages/public/serviceApi.php?action=create_order" \
  -H "X-Api-Key: sk_YOURKEY" -H "Content-Type: application/json" \
  -d '{"service_type":"extraction","request":"Extract each line item as fields sku, name, price","deliverable_format":"json","source_docs":"<your document text here>"}'
# deliverable (when done) is a ready-to-parse JSON array, e.g.:
# [{"sku":"A1","name":"Blue Mug","price":4.99},{"sku":"A2","name":"Green Mug","price":4.99}]
```

Poll status, then fetch the result:
```bash
curl -s "https://mysqlbot.com/pages/public/serviceApi.php?action=status&ref=SO-XXXX" -H "X-Api-Key: sk_YOURKEY"
curl -s "https://mysqlbot.com/pages/public/serviceApi.php?action=deliverable&ref=SO-XXXX" -H "X-Api-Key: sk_YOURKEY"
```

## 6. Errors
| HTTP | error | meaning |
|------|-------|---------|
| 401 | `invalid_api_key` | missing/unknown/disabled key |
| 402 | `insufficient_balance` | prepaid key: balance too low — `data.balance`/`data.required`/`data.top_up_url`; top up then retry |
| 429 | `rate_limited` | per-key rate limit (default 20/min) — slow down |
| 429 | `provision_rate_limited` | too many `register` calls (per-IP **or** site-wide hourly cap) — reuse your key |
| 429 | `daily_order_cap_reached` | per-key daily order cap hit (`data.cap`, default 200/day) |
| 400 | `invalid_service_type` / `empty_request` / `extraction_requires_source_docs` | fix the request |
| 400 | `missing_ref` / `unknown_action` | supply `ref=` / use a valid `action=` |
| 400 | `invalid_email` | `register`: the `email` you supplied isn't a valid address |
| 413 | `source_docs_too_long` | `source_docs` over 300,000 chars (`data.max`) — split the input |
| 404 | `order_not_found` | wrong ref, or not your key's order |
| 409 | `not_ready` | poll `status` until `done`, then retry |
| 409 | `order_refunded` | the order was refunded; no deliverable |
| 405 | `use_POST_for_create_order` / `use_POST_for_register` | that action must be POST |
| 503 | `service_price_unconfigured` | that service's price isn't set — contact us |
| 500 | `create_failed` / `provision_failed` / `server_error` | transient; retry with backoff |
| 403 | `no_permission` | your key's member lacks the permission for that management action |
| 403 | `not_an_account_key` | the key isn't attached to a team account (a `register` key is standalone) |
| 403 | `key_member_not_resolved` | the key's owning member couldn't be resolved on its account |
| 409 | `already_a_member_somewhere` | that email already belongs to an account (one account per email) |
| 404 | `member_not_found` / `key_not_found` | that member/key isn't on your account |
| 429 | `account_key_cap_reached` | this account has hit its active-key limit (`data.cap`) — revoke unused keys |
| 400 | `name_required` / `password_min_10` / `cannot_remove` / `cannot_change_owner` / `update_failed` | management input problem |

## 7. Guarantees & limits
- Research/competitor outputs are **web-verified with cited sources**; they are informational,
  not professional (legal/financial/medical/tax) advice.
- Extraction reads the **text you provide** — it never invents values.
- One rig fulfils orders serially, so heavy concurrent load queues rather than fails.
- `deliverable` is available only while the order is retained; fetch it promptly.

## 8. Team & account management (full parity with the control panel)
An API key that belongs to a **team account** (one created in the client portal) can manage that
account programmatically. The key acts with the **permissions of the member who created it**, so the
API enforces exactly the same gates as the web control panel — nothing more, nothing less. (A
self-provisioned `register` key is standalone with no team; these actions return `403
not_an_account_key`.)

Discover your own authority first:
- `GET ?action=account` →
  `{account:{id,name,currency,status}, member:{id,email,role,display_name},
    permissions:{is_owner,can_see_billing,can_see_all_orders,can_manage_keys,can_manage_members}}`.

Actions (each needs the permission shown; an **owner** implicitly has all four flags):

| action | method | permission | body | does |
|--------|--------|-----------|------|------|
| `account` | GET | any account key | — | who am I + my permissions |
| `list_members` | GET | can_manage_members | — | team roster + each member's permissions |
| `invite_member` | POST | can_manage_members | `email`, `permissions:{flag:true,…}`, `send_invite?` (default true) | add a member (emails a sign-in link). **Least-privilege:** you may only grant flags you hold. |
| `remove_member` | POST | can_manage_members | `member_id` | remove a member + end their sessions (a non-owner can't remove an owner; never the last owner) |
| `set_permissions` | POST | **owner only** | `member_id`, `permissions:{flag:true,…}` | set a member's granular permissions (an owner's own row can't be changed) |
| `list_keys` | GET | can_manage_keys | — | the account's API keys (prefix, label, billing, balance, active, usage) |
| `create_key` | POST | can_manage_keys | `label?`, `billing?` (`account` default \| `prepaid`) | mint a new key on the account (plaintext shown **once**; a `prepaid` key also returns `top_up_url`) |
| `revoke_key` | POST | can_manage_keys | `key_id` | deactivate one of the account's keys |
| `rename_account` | POST | **owner only** | `name` | rename the account |
| `set_display_name` | POST | self | `display_name` | set your own display name |
| `set_password` | POST | self | `password` (10–200 chars) | set your own password (ends your other sessions) |
| `list_orders` | GET | any key | `limit?` (≤200) | an **account** key → account orders (a member without `can_see_all_orders` sees only their own); a standalone **prepaid** key → the orders **it** created. `data.scope` = `all`\|`own`\|`key` |

The permission flags are `can_see_billing`, `can_see_all_orders`, `can_manage_keys`,
`can_manage_members` (plus the implicit `is_owner`).

> **Security:** an account-management key is as powerful as the member behind it — an owner's key can
> invite/remove members, change permissions, and mint keys. Treat such a key like a password: scope
> each key to the least privilege needed, and `revoke_key` any that leak.

## 9. Full worked example (end to end)
The whole journey for an autonomous agent: **sign up → get funded once → place an order → poll until
it's ready → read the result → review what's been processed** (with an optional team-management block).
Both listings below are runnable and were verified against the live API.

### 9a. PHP (uses the built-in `curl` extension — no dependencies)
```php
<?php
// MySQLBot Services API — full worked example. PHP 7.4+ / 8.x.
const API = 'https://mysqlbot.com/pages/public/serviceApi.php';

/**
 * One API call. GET params go in $query; a JSON body (POST) goes in $body.
 * Returns [httpStatus, decodedArray]. Auth is the X-Api-Key header (omit for `register`).
 */
function call(string $action, string $method = 'GET', ?array $body = null, ?string $apiKey = null, array $query = []): array {
    $url = API . '?action=' . rawurlencode($action);
    foreach ($query as $k => $v) { $url .= '&' . rawurlencode((string)$k) . '=' . rawurlencode((string)$v); }
    $headers = ['Accept: application/json'];
    if ($apiKey !== null) { $headers[] = 'X-Api-Key: ' . $apiKey; }
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_TIMEOUT => 60]);
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $raw  = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return [$code, json_decode((string)$raw, true) ?: []];
}

// 1) SIGN UP — self-provision a prepaid key (no auth). api_key is shown ONCE: store it now.
[$code, $r] = call('register', 'POST', ['name' => 'my agent', 'email' => 'you@example.com']);
if (($r['ok'] ?? false) !== true) { exit('register failed: ' . ($r['error'] ?? $code) . "\n"); }
$key = $r['data']['api_key'];
echo "Store this key: {$key}\n";
echo "Ask a human to add credit here (one time): {$r['data']['top_up_url']}\n";

// 2) FUND — wait until a human has topped the balance up via that link.
do {
    sleep(10);
    [, $b] = call('balance', 'GET', null, $key);
    $balance = (float)($b['data']['balance'] ?? 0);
    echo 'Balance: ' . ($b['data']['currency'] ?? '') . ' ' . number_format($balance, 2) . "\n";
} while ($balance <= 0);

// 3) PLACE AN ORDER. idempotency_key makes a timed-out retry safe (never double-billed).
[$code, $o] = call('create_order', 'POST', [
    'service_type'    => 'research_brief',               // or 'competitor_brief' | 'extraction'
    'request'         => 'Summarise the UK EV grant landscape with sources',
    'idempotency_key' => 'ev-grants-2026-06-01',
], $key);
if ($code === 402) { exit("Out of credit — top up: {$o['data']['top_up_url']}\n"); }
if (($o['ok'] ?? false) !== true) { exit('create failed: ' . ($o['error'] ?? $code) . "\n"); }
$ref = $o['data']['order_ref'];
echo "Order {$ref} placed ({$o['data']['currency']} {$o['data']['amount']} debited).\n";

// 4) POLL until the order is finished. Poll politely (every 15–30s).
do {
    sleep(20);
    [, $s] = call('status', 'GET', null, $key, ['ref' => $ref]);
    $status = $s['data']['status'] ?? 'unknown';
    echo "Status: {$status}\n";
} while (!in_array($status, ['done', 'failed', 'cancelled'], true));
if ($status !== 'done') { exit("Order {$status}. Any charge is auto-refunded to your balance.\n"); }

// 5) READ THE RESULT — returned inline (markdown / CSV / JSON per deliverable_format).
[, $d] = call('deliverable', 'GET', null, $key, ['ref' => $ref]);
echo "--- Result ({$d['data']['deliverable_format']}) ---\n{$d['data']['deliverable']}\n";

// 6) REVIEW everything processed on this key.
[, $list] = call('list_orders', 'GET', null, $key, ['limit' => 50]);
foreach (($list['data']['orders'] ?? []) as $ord) {
    echo "{$ord['order_ref']}  {$ord['service_type']}  {$ord['status']}\n";
}
```

**Managing a team account** (use an ACCOUNT key created in the portal, not a `register` key):
```php
// Discover your authority, invite a teammate, then set their permissions.
[, $me]  = call('account', 'GET', null, $accountKey);            // {member:{role}, permissions:{…}}
[, $inv] = call('invite_member', 'POST',                          // needs can_manage_members
                ['email' => 'mate@example.com', 'permissions' => ['can_see_all_orders' => true]], $accountKey);
call('set_permissions', 'POST',                                   // owner only
     ['member_id' => $inv['data']['member_id'], 'permissions' => ['can_see_all_orders' => true, 'can_see_billing' => true]], $accountKey);
[, $keys] = call('list_keys', 'GET', null, $accountKey);          // needs can_manage_keys
```

### 9b. C (libcurl)
```c
// Build: cc example.c -o example -lcurl
#include <stdio.h>
#include <curl/curl.h>

static size_t sink(char *ptr, size_t size, size_t nmemb, void *userdata) {
    fwrite(ptr, size, nmemb, stdout);          /* print the JSON response */
    return size * nmemb;
}

int main(void) {
    CURL *c = curl_easy_init();
    if (!c) return 1;

    struct curl_slist *h = NULL;
    h = curl_slist_append(h, "Content-Type: application/json");
    h = curl_slist_append(h, "Accept: application/json");
    h = curl_slist_append(h, "X-Api-Key: sk_YOURKEY");   /* OMIT for action=register */

    /* JSON body — escape the inner quotes. */
    const char *body =
        "{\"service_type\":\"research_brief\","
        "\"request\":\"Summarise the UK EV grant landscape with sources\"}";

    curl_easy_setopt(c, CURLOPT_URL,
        "https://mysqlbot.com/pages/public/serviceApi.php?action=create_order");
    curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
    curl_easy_setopt(c, CURLOPT_POSTFIELDS, body);       /* a body ⇒ POST */
    curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, sink);

    CURLcode rc = curl_easy_perform(c);
    if (rc != CURLE_OK) fprintf(stderr, "\ncurl: %s\n", curl_easy_strerror(rc));

    curl_slist_free_all(h);
    curl_easy_cleanup(c);
    return 0;
}
```
Parse the `{ok,error,data}` envelope with any C JSON library (e.g. cJSON):
```c
cJSON *j    = cJSON_Parse(response);
cJSON *data = cJSON_GetObjectItem(j, "data");
const char *ref = cJSON_GetStringValue(cJSON_GetObjectItem(data, "order_ref"));
/* GET calls (status / deliverable / balance / list_orders) put params in the query string, e.g.
   ".../serviceApi.php?action=status&ref=SO-XXXX", and keep the X-Api-Key header. */
```
