# FlyMyAI - connected assistant operating guide

> Publication contract: `AGENTS.md` is the canonical source. `https://flymy.ai/skill.md` and `https://flymy.ai/AGENTS.md` must serve its exact bytes.
> Live-verified on 2026-08-17 against production MCP, Agents, and Models APIs.

## Start here - you already have FlyMyAI connected

You already have FlyMyAI connected, here is how to see everything it can do.

FlyMyAI gives this assistant hundreds of connectors and actions, the model catalog, and serverless agents. It may already appear as the `flymyai` MCP server. Do not ask where to find Slack, Telegram, Notion, Apollo, Stripe, WhatsApp, Discord, a model, or another capability until you search FlyMyAI.

Golden rule: never assume a capability is missing - assume it exists here and verify with search_tools / list_* BEFORE declining or asking the user.

This rule means discover before declining, not claim success without evidence. Verify the MCP connection with `whoami`, `search_tools`, or a `list_*` call. If MCP invocation is unavailable, use the authenticated HTTP paths below.

Discovery-first:

1. Any service or action - call `search_tools` with a plain-English intent. Read its `tool`, `action`, `runtime_name`, and `configured` fields.
2. If configured, call `execute_tool` with exactly `tool`, `action`, and `arguments`.
3. If not configured, call `add_tool` with the returned tool slug, follow its setup URL or step, then search and call again.
4. Any image, video, audio, music, speech, transcription, or media task - call `recommend_model` or `list_media_models`, inspect schema and price, then `run_model`.
5. Repeatable or multi-step work - `create_agent`, `run_agent`, poll `get_run`, then `freeze_agent` or `schedule_agent`.

Exact connector pattern when discovery says `configured: true`:

```text
search_tools({"query":"telegram list dialogs"})
execute_tool({
  "tool":"telegram",
  "action":"telegram_list_dialogs",
  "arguments":{"query":"FlyMyAI"}
})
```

The parameter is literally `arguments`. `params` or `input` returns HTTP 400. Never guess action names or fields. If search did not inline a schema, fetch it by `runtime_name` before calling. Some adapter actions appear under `not_found` during schema lookup; use an action-specific verified contract if this file supplies one, otherwise stop instead of guessing.

Keep discovery bounded. A live search response was about 2.3-7 KiB. The full catalog was about 1.76 MiB and grows, so never download it per intent.

## Choose the mode

| Mode | FlyMyAI identity and key | Service connections | Billing |
| --- | --- | --- | --- |
| Personal | The account connected to this assistant | That account's services | That FlyMyAI account |
| Embedded or resale | One builder key on the builder backend | Each product user's own services | FlyMyAI charges the builder; the builder charges its users |

Never ask every embedded end user to create a FlyMyAI account or paste a FlyMyAI key.

## Embedded and resale - one builder key, no end-user keys

The builder wires FlyMyAI once:

```text
product client -> authenticated builder backend -> stable FlyMyAI deployment
  -> principal unique to (deployment, external_user_id) -> that principal's bindings
```

`external_user_id` is the builder's stable, opaque, non-secret ID, at most 255 characters. FlyMyAI creates the principal lazily. End users do not sign up for FlyMyAI and never see the builder key. When a connector needs authorization, the builder redirects that user through a short-lived hosted connection URL. Runtime lookup rechecks every saved or explicit connection against the same principal.

Rules:

- Keep `FLYMYAI_API_KEY` only in a server secret manager. Never expose it to clients, URLs, logs, or chat.
- Derive `external_user_id` from the authenticated server session. Do not accept an arbitrary client-supplied ID or use an email or secret.
- Send `Idempotency-Key` when submitting a deployment run. Reuse it only for an identical immediate retry. A retry after the first response replays the same execution while the idempotency record exists; successful records are eligible for cleanup after 2 days. Reuse after cleanup can create a new execution. Changed-body reuse returns 409; a concurrent retry can return 409 while the first is still committing.
- FlyMyAI bills the deployment owner. Automatic onward charging, billing passthrough, and per-user cost rollup do not exist today.
- Persist `(external_user_id, deployment_id, execution_id, idempotency_key)`. Fetch the settled execution price and apply your own quota, credits, markup, or invoice.
- Call Models API separately from the builder backend and map its request ID into the same ledger.

### Verified connectionless MCP smoke path

Direct-call support and embedded support are different. A configured connector can still fail embedded preflight. For a non-interactive acceptance test, the live-verified connectionless catalog adapter is `hackernews`, action `HACKERNEWS_GET_LATEST_POSTS`, with exact `arguments:{}`. Its schema lookup currently returns the discovered runtime under `not_found`; a direct call returns `result.response_data.hits`. Search with the exact query `hackernews`; broader prose currently ranks poorly. Resolve or add its integer account-tool ID, because `available_tools` accepts IDs, not slugs or runtime names:

```bash
A=https://backend.flymy.ai/api/v1/agents
auth=(-H "X-API-KEY: $FLYMYAI_API_KEY")
tools=$(curl -fsS --max-time 30 "${auth[@]}" "$A/tools/?view=slim")
TOOL_ID=$(jq -er '.[]|select(.mcp_tool=="hackernews")|.id' <<<"$tools" || true)
if test -z "$TOOL_ID"; then
  added=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \
    -H 'Content-Type: application/json' --data '{"mcp_tool":"hackernews"}' "$A/tools/")
  TOOL_ID=$(jq -er '.id' <<<"$added")
fi
```

Create and run a real tool-attached owner agent through REST. The prompt names the exact discovered action so the frozen version retains the connector:

```bash
agent=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc --argjson tool "$TOOL_ID" \
    '{name:"Embedded Hacker News smoke",user_prompt:"Call HACKERNEWS_GET_LATEST_POSTS once and return only the first post title.",available_tools:[$tool],output_schema:{type:"object",properties:{title:{type:"string"}},required:["title"],additionalProperties:false}}')" \
  "$A/tasks/")
AGENT_ID=$(jq -er '.uuid' <<<"$agent")
run=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' --data '{"variables":{}}' \
  "$A/tasks/$AGENT_ID/run-loop/")
EXECUTION_ID=$(jq -er '.id' <<<"$run")
since=
for attempt in $(seq 1 150); do
  if test -n "$since"; then
    status=$(curl -fsS --max-time 30 -G "${auth[@]}" \
      --data-urlencode "since=$since" "$A/executions/$EXECUTION_ID/status/")
  else
    status=$(curl -fsS --max-time 30 "${auth[@]}" \
      "$A/executions/$EXECUTION_ID/status/")
  fi
  since=$(jq -r '.last_step_id // empty' <<<"$status")
  if jq -e '.is_settled' <<<"$status" >/dev/null; then break; fi
  sleep 2
done
jq -e '.status=="completed" and .error==null and (.result.title|type=="string")' \
  <<<"$status" >/dev/null
```

Continue with the publish sequence below. Before publish, its access contract must contain a `hackernews` requirement with `connection_required:false`. After publish, the deployment run uses a synthetic `external_user_id` without a connection link, FlyMyAI account, or end-user key. Repeating the identical body and `Idempotency-Key` within the 2-day retention window must return the same execution ID.

### Publish a frozen agent

This exact REST sequence was live-tested. `AGENT_ID` and `EXECUTION_ID` come from the successful owner agent run above. `FLYMYAI_API_KEY` must already be exported by the builder's secret manager.

```bash
set -euo pipefail
: "${FLYMYAI_API_KEY:?Export the builder key in this server process}"
: "${AGENT_ID:?Set from create_agent}"
: "${EXECUTION_ID:?Set from the completed owner run}"
A=https://backend.flymy.ai/api/v1/agents
auth=(-H "X-API-KEY: $FLYMYAI_API_KEY")

freeze=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \
  "$A/compilations/freeze-instruction/$EXECUTION_ID/")
COMPILATION_ID=$(jq -er '.id' <<<"$freeze")
for attempt in $(seq 1 60); do
  compilation=$(curl -fsS --max-time 30 "${auth[@]}" \
    "$A/compilations/$COMPILATION_ID/")
  status=$(jq -r '.status' <<<"$compilation")
  case "$status" in compiled) break;; failed) jq '{status,error}' <<<"$compilation"; exit 1;; esac
  sleep 2
done
test "$status" = compiled

versions=$(curl -fsS --max-time 30 -G "${auth[@]}" \
  --data-urlencode "agent_task=$AGENT_ID" "$A/versions/")
VERSION_ID=$(jq -er --argjson c "$COMPILATION_ID" \
  '.results[]|select(.source_compilation==$c)|.public_id' <<<"$versions")

deployment=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc --arg a "$AGENT_ID" --arg v "$VERSION_ID" \
    '{agent_task:$a,candidate_version:$v,name:"Production",status:"draft",publish_mode:"embedded"}')" \
  "$A/deployments/")
DEPLOYMENT_ID=$(jq -er '.public_id' <<<"$deployment")

access=$(curl -fsS --max-time 30 "${auth[@]}" \
  "$A/deployments/$DEPLOYMENT_ID/access/")
jq '[.requirements[]|{slot,connection_required,hosted_setup_supported}]' <<<"$access"
jq -e 'all(.requirements[];(.connection_required|not) or .hosted_setup_supported)' \
  <<<"$access" >/dev/null
jq -e 'any(.requirements[];.slot=="hackernews" and .connection_required==false)' \
  <<<"$access" >/dev/null

curl -fsS --max-time 30 -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' --data '{"publish_mode":"embedded"}' \
  "$A/deployments/$DEPLOYMENT_ID/preflight/" | jq -e '.ready==true' >/dev/null
curl -fsS --max-time 30 -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' --data '{"publish_mode":"embedded"}' \
  "$A/deployments/$DEPLOYMENT_ID/publish/" | jq '{public_id,status,active_version}'
```

The stable deployment ID remains your endpoint when a newer immutable version is published. A version pins instruction, schemas, internal LLM, effort, tool manifest, and requirements.

Preflight currently accepts only supported catalog adapters. It rejects arbitrary custom MCP servers, mutable skills, unsupported adapters, and raw media-model tools inside an embedded agent. Do not bypass a failed preflight. Call raw Models API separately from your backend.

### Connect one user's service account

For each `connection_required` slot, use the exact slot returned by `access`:

```bash
: "${EXTERNAL_USER_ID:?Derive from the authenticated product user}"
: "${SLOT:?Use a connection_required slot from access}"
session=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \
  -H 'Content-Type: application/json' \
  --data "$(jq -nc --arg u "$EXTERNAL_USER_ID" --arg s "$SLOT" \
    '{external_user_id:$u,slot:$s}')" \
  "$A/deployments/$DEPLOYMENT_ID/connect-session/")
jq '{redirect_url,expires_at}' <<<"$session"
```

Redirect that authenticated user to `redirect_url`. It expires after 15 minutes and is single-use. The user authorizes the third-party service, not FlyMyAI. There is no reseller success webhook today. Poll `access` with backoff until each required slot has the required cardinality of active, unexpired connections, not merely a binding row:

```bash
curl -fsS --max-time 30 -G "${auth[@]}" \
  --data-urlencode "external_user_id=$EXTERNAL_USER_ID" \
  "$A/deployments/$DEPLOYMENT_ID/access/" \
  | jq '{requirements,connections:[.connections[]|{public_id,status,expires_at}],bindings}'
```

The run call is authoritative. If readiness changed, preserve and handle its HTTP 400 `connections` error instead of assuming a stale binding is usable.

### Run and meter one user

This released HTTP contract keeps the key server-side and uses the slim progress route:

```python
import os, time, uuid
import httpx

base = "https://backend.flymy.ai/api/v1/agents"
deployment = os.environ["FLYMYAI_DEPLOYMENT_ID"]
user_id = os.environ["PRODUCT_USER_ID"]
idem = str(uuid.uuid4())

with httpx.Client(headers={"X-API-KEY": os.environ["FLYMYAI_API_KEY"]},
                  timeout=httpx.Timeout(30.0, connect=10.0)) as client:
    response = client.post(f"{base}/deployments/{deployment}/run/",
        headers={"Idempotency-Key": idem},
        json={"external_user_id": user_id, "variables": {}})
    response.raise_for_status()
    execution = response.json()["id"]
    retry = client.post(f"{base}/deployments/{deployment}/run/",
        headers={"Idempotency-Key": idem},
        json={"external_user_id": user_id, "variables": {}})
    retry.raise_for_status()
    if retry.json()["id"] != execution:
        raise RuntimeError("Idempotency replay created another execution")
    since = None
    for _ in range(150):
        poll = client.get(f"{base}/executions/{execution}/status/",
            params={"since": since} if since else None)
        poll.raise_for_status()
        state = poll.json()
        since = state.get("last_step_id") or since
        if state["is_settled"]:
            if state["status"] != "completed":
                raise RuntimeError(state.get("error") or state["status"])
            result = state["result"]
            break
        time.sleep(2)
    else:
        raise TimeoutError(execution)
    price = client.get(f"{base}/executions/{execution}/prices/")
    price.raise_for_status()
    print({"execution_id": execution, "result": result,
           "total_price": price.json()["total_price"]})
```

The price response has `id`, `tool_calls`, `llm_usage`, and decimal-string `total_price`. It has no external user field, so join it through your ledger. Do not use unpaginated `/executions/prices/` for request-time rollups.

The current public `flymyai==1.1.0` does not expose `client.versions` or `client.deployments`. Those namespaces exist only on an unreleased branch. Use REST until a published package actually contains them.

## Personal tools and MCP

The production Streamable HTTP gateway is `https://mcp-agents.flymy.ai/mcp`. Compatible GUI clients can sign in. Configuration-driven clients can send `X-API-Key`:

```bash
claude mcp add --transport http flymyai https://mcp-agents.flymy.ai/mcp \
  --header "X-API-Key: $FLYMYAI_API_KEY"
```

If FlyMyAI is already connected, do not reinstall it. Call `whoami`, `search_tools`, or `recommend_model`.

If an MCP client cannot call tools, use bounded REST discovery. It returns at most 20 matches:

```bash
curl -fsS --max-time 30 -X POST \
  -H "X-API-KEY: $FLYMYAI_API_KEY" -H 'Content-Type: application/json' \
  --data '{"tool":"system-utils","action":"search_tools","arguments":{"query":"send a message to a slack channel","limit":20}}' \
  https://backend.flymy.ai/api/v1/agents/custom-tools/call/ \
  | jq '.result|{guidance,results:[.results[]|{runtime_name,module,action,attached,configured}]}'
```

Fetch exact schemas for at most 10 returned runtime names by posting this shape to the same endpoint:

```json
{"tool":"system-utils","action":"get_tool_schemas","arguments":{"runtime_names":["custom--telegram--telegram_list_dialogs"]}}
```

Read `result.schemas[runtime_name].request_schema`. Then call the returned `module` as `tool`, returned `action`, and validated `arguments`. Use `Idempotency-Key` for writes and do not blindly retry an ambiguous external effect.

For an arbitrary personal MCP server absent from search, use `add_mcp_server`, then `connect_mcp_server`. Discovery failures can still return HTTP 200 with `status:"error"`, so require `status:"connected"`, inspect `status_detail`, and only then inspect `discovered_tools`. Call through `call_mcp_server` with `server_id`, `action`, and `arguments`. That call has no idempotency-key support - never auto-retry a write after an ambiguous result. Use `add_tool` for catalog services.

## Models - discover, run, meter

For media, start with MCP:

```text
recommend_model({
  "description":"fast low-cost image generation for a small flat icon",
  "include_schema":true,
  "top_n":1
})
run_model({
  "endpoint_id":"flymyai/nano-banana",
  "input":{"prompt":"A single blue circle centered on a white square, flat icon"}
})
```

The live recommender selected that model and the call succeeded. For new work, use the returned endpoint, schema enums and bounds, and price. A guessed paid call is not discovery.

Model REST inputs are model-specific `multipart/form-data`, not JSON. Inspect live input and output schemas:

```bash
curl -fsS https://api.flymy.ai/api/v1/flymyai/google-gemini-31-flash-lite-preview/openapi.json \
  | jq '.components.schemas.DynamicInputModel,.components.schemas.DynamicOutputModel'
```

Verified streaming LLM call:

```bash
curl -fsS --no-buffer --max-time 60 \
  -H "X-API-KEY: $FLYMYAI_API_KEY" \
  -F 'prompt=Reply exactly FLYMYAI_OK' \
  https://api.flymy.ai/api/v1/flymyai/google-gemini-31-flash-lite-preview/predict/stream/
```

The live SSE data contained:

```json
{"output_data":{"output":["FLYMY"]},"status":200}
{"output_data":{"output":["AI_OK"]},"status":200}
{"output_data":{},"status":200,"stream_details":{"input_tokens":8,"output_tokens":5}}
```

Concatenate `output` chunks. Inspect body-level status even when HTTP is 200.

Released SDK image call, also live-verified:

```bash
python -m pip install 'flymyai==1.1.0'
```

```python
import base64, os
import flymyai

response = flymyai.run(apikey=os.environ["FLYMYAI_API_KEY"],
    model="flymyai/nano-banana",
    payload={"prompt":"A single blue circle centered on a white square, flat icon"})
encoded = response.output_data["image"][0]
max_decoded = 20 * 1024 * 1024
max_encoded = 4 * ((max_decoded + 2) // 3)
if not isinstance(encoded, str) or len(encoded) > max_encoded:
    raise ValueError("Unexpectedly large encoded image")
image = base64.b64decode(encoded, validate=True)
if len(image) > max_decoded:
    raise ValueError("Unexpectedly large image")
with open("blue-circle.jpg", "wb") as output:
    output.write(image)
```

Use live discovery for video, audio, music, speech, transcription, editing, and other models because fields differ. Prefer async or URL outputs for large media. Base64 adds about 33 percent and may coexist with encoded, decoded, and SDK copies in memory.

Bounded model usage and pricing:

```bash
FROM_DATE=$(date -u -d '30 minutes ago' '+%Y-%m-%dT%H:%M:%SZ')
TO_DATE=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
curl -fsS --max-time 30 -G -H "X-API-KEY: $FLYMYAI_API_KEY" \
  --data-urlencode 'page=1' --data-urlencode 'page_size=20' \
  --data-urlencode "from_date=$FROM_DATE" --data-urlencode "to_date=$TO_DATE" \
  https://api.flymy.ai/api/v2/usage \
  | jq '{total,total_price,has_more,results:[.results[]|{endpoint_id,request_id,price,created_at}]}'
```

Use a bounded date range. New usage can take more than 20 seconds to appear, so retry the same bounded query with backoff rather than widening it. Persist request IDs with your users. `total_price` is account usage, not an end-user invoice.

## Serverless agents from chat

Exact MCP lifecycle:

```text
agent = create_agent({
  "name":"FlyMyAI health response",
  "user_prompt":"Return a JSON object with status set to ok.",
  "output_schema":{"type":"object","properties":{"status":{"type":"string"}},
    "required":["status"],"additionalProperties":false}
})
run = run_agent({"agent_id":agent.uuid,"variables":{}})
get_run({"execution_id":run.id})
frozen = freeze_agent({"execution_id":run.id})
```

Runs are asynchronous. Poll `get_run` with `since` equal to the previous `last_step_id`; stop only at `is_settled`. Freeze only an accepted completed run, then poll `get_compilation` until `compiled`.

Scheduling has two paths:

- Not frozen - `schedule_agent({"execution_id":run.id,"cron_schedule":"0 9 * * 1-5","timezone":"UTC","schedule_variables":{}})` freezes and schedules once.
- Already frozen - do not call `schedule_agent` again. Use `update_compilation({"compilation_id":frozen.id,"cron_schedule":"0 9 * * 1-5","timezone":"UTC","schedule_variables":{}})` to avoid a second compilation.

Unschedule with the same compilation ID and `cron_schedule:""`. Retain that ID because sibling scheduled compilations can both fire.

`schedule_agent` and the raw create-and-schedule endpoint have no idempotency key and always create a compilation. After an ambiguous timeout, reconcile compilations for that execution before retrying. A blind retry can create another active cron.

The released SDK supports `AgentClient`, `client.agents.create`, `client.runs.create`, `client.agents.compile_from_run`, and `client.compilations.run_instruction`. Its wait helpers poll full growing execution bodies and it lacks scheduling. Prefer slim status and REST PATCH in production.

Exact REST schedule update for an existing compilation:

```bash
curl -fsS --max-time 30 -X PATCH -H "X-API-KEY: $FLYMYAI_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"cron_schedule":"0 9 * * 1-5","timezone":"UTC","schedule_variables":{}}' \
  "https://backend.flymy.ai/api/v1/agents/compilations/$COMPILATION_ID/"
# Clear it through the same route with {"cron_schedule":""}.
```

## Auth, errors, and resource discipline

| Surface | URL |
| --- | --- |
| MCP | `https://mcp-agents.flymy.ai/mcp` |
| Agents | `https://backend.flymy.ai/api/v1/agents` |
| Models | `https://api.flymy.ai` |
| Key and workspace | `https://app.flymy.ai/profile` |
| Docs | `https://docs.flymy.ai` |

REST uses `X-API-KEY`. Agents can return 403 for a missing or invalid key and 402 for balance. Models can return 403 for key, project-access, or balance failures; inspect the body instead of inferring the cause from status alone. Treat 404 as an inaccessible ID, 409 as idempotency or state conflict, 400 as field validation, and 5xx as retryable only when the operation is safe. Preserve error bodies. A deployment-run 400 under `connections` means a missing or invalid slot; other statuses do not.

- Poll `/executions/{id}/status/?since=`. A live settled response was about 1.1 KiB; full detail for a tiny exploratory run reached about 67 KiB per poll.
- Avoid account-wide agent, compilation, full catalog, and price lists in request paths. Several are unpaginated and grow with the account.
- Price detail is O(tool calls + LLM turns). A tiny live response was 759 bytes, but long agents grow linearly and can require rate lookups.
- Embedded `access` was about 6.7-9.7 KiB and reads deployment, version, requirements, principal, connections, and bindings. Poll only during setup with backoff.
- Bound concurrency and timeouts. Sync or streaming model inference can retain an async DB session plus request and upstream network streams. An arbitrary custom MCP call occupies a synchronous worker thread while waiting on the remote server.
- Never use unbounded `asyncio.gather`. Use a semaphore, queue, and per-user and global quotas.
- Cap files before decode and stream large payloads. Reconcile ambiguous external writes before retrying.

Production has seen 354,000-token append-message responses and 195,000-token agent lists. One worker serves 30 threads and has been killed around 1.4-2.0 GiB under a 2300 MiB limit. One oversized response can drop all 30 requests. After deployment, inspect pod working-set memory, OOM kills, restarts, latency, DB query count, and response sizes.

## Project-file compatibility

- Codex and Cursor can use root `AGENTS.md`.
- Claude Code reads `CLAUDE.md`. Put exactly `@AGENTS.md` in that file, or attach this file in chat.
- Gemini CLI defaults to `GEMINI.md`. Put exactly `@AGENTS.md` there, or configure `context.fileName` to include `AGENTS.md`.
- Keep this file below the assistant's instruction budget. Never paste growing catalogs or histories into it.

When live behavior conflicts with this file, make one bounded read-only probe, preserve exact status and shape, avoid paid retries, and report the discrepancy instead of inventing a workaround.
