# 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. > Base lifecycle live-verified on 2026-08-27. MCP resource-scope additions are > a 2026-08-28 release candidate; run both early gates before promotion and > repeat the clean-agent gate after production promotion. > These candidate guide bytes are aligned to Agents MCP `0.7.0-rc.3` and > Python SDK `1.2.0rc4`. Capability-detect the installed package and live MCP > schemas instead of assuming an older or newer artifact has this contract. Release maintainers must first fetch the exact guide origin's `/agent-guide-release.json`. Its compatible outer schema-v1 guide projection contains one `release_contract` with schema version 2. Before any product mutation, the release harness must require the intended candidate channel and coordinates, recompute the product bundle and candidate tuple hashes, compare the tuple to an independently approved release pin, and verify the pinned bytes for `/skill.md`, `/llms.txt`, the OpenAPI URL, `/agents-mcp-tool-schema.json`, and `/embedded-capabilities.json`. It must also verify the declared Agents MCP package and image, Python SDK wheel and minimum version, consumer identities, image digests, and lifecycle evidence state. Missing members, stale hashes, incompatible versions, blocked coordinates, or a candidate/stable mismatch stop the release gate. This tuple check is release machinery; a personal user connecting one account does not need to learn it. ## Start here - verify FlyMyAI or recover FlyMyAI gives this assistant hundreds of connectors and actions, the model catalog, and serverless agents. It may already appear as the `flymyai` MCP server. Make one bounded read-only call such as `whoami`, `tools/list`, `search_tools`, or a `list_*` tool before claiming it is connected. If the server or tools are absent, do not claim success and do not guess tool names. Ask the user to connect the exact intended environment. GUI clients can use OAuth sign-in. Configuration-driven clients use the exact `FLYMYAI_MCP_URL` and an `X-API-Key` supplied from their secret manager, never pasted into chat. If MCP remains unavailable, use REST only after the caller supplies an authenticated `FLYMYAI_AGENTS_API_ROOT`; otherwise report that preflight is blocked. A release-candidate run must use an explicit candidate release contract, MCP, skill, and Agents API roots and must never fall back to production. Do not ask where to find Slack, Telegram, Notion, Apollo, Stripe, WhatsApp, Discord, a model, or another capability until you search FlyMyAI in the verified environment. ## Ship one agent from idea to product When the FlyMyAI Agents MCP connection is available, author through its tools by name - `create_agent`, `run_agent`, `get_run`, `freeze_agent`, `get_compilation`, `run_frozen` - and do not drive the authoring lifecycle over raw REST calls. REST and the SDK are the embedding surfaces for the published deployment inside your product, not a substitute authoring path. Credential hygiene is part of the contract: never print, echo, dump, or read back credentials or the process environment (no `env`, `printenv`, or `echo $KEY` inspection); reference secrets only as variables inside the commands that need them. The default production outcome is one complete logical agent, not a chain of partial agents: ```text create one agent -> test live runs -> freeze one accepted run -> ship it |-> owner/private call |-> customer deployment ``` Use this lifecycle unless the user explicitly asks for a multi-agent system: 1. Define one complete worker: goal, tools, inputs, and output contract. 2. Call `create_agent` once. Keep the returned agent UUID. 3. Reserve a caller-owned, nonblank printable ASCII `operation_key` of 1-255 characters with no leading or trailing spaces, call `run_agent` with it and a realistic input, then poll `get_run` until `poll_complete=true`. Refine that same agent with a new key for each new logical run until the user accepts one completed execution. 4. Call `freeze_agent` with the accepted execution ID. Poll `get_compilation` until `status=compiled`. If that frozen artifact is then test-run, the projected compilation status may become `running`, `completed`, or `failed`; `completed` is also publish-ready. 5. Return the agent UUID, accepted execution ID, integer compilation ID, `instruction_md`, input/output schemas, and ready integration code. 6. Test the frozen artifact with `run_frozen` and another caller-owned `operation_key` before calling it production-ready. 7. For an embedded release, have the trusted release harness call the same deployment for two controlled customer identities after the assistant has shipped it. Normal product IDs come from authenticated server sessions; the harness may mint synthetic IDs only inside its trusted test identity store. Do not put those IDs in the model prompt or MCP tool arguments. Require distinct principal and execution IDs while keeping the same agent, frozen version, deployment, and billed owner. Replay each identical request with its original idempotency key and require the original execution ID. Variables do not require another agent. For a fixed worker, omit `input_schema` and placeholders. For a reusable worker, define `input_schema`, put matching `{{ placeholders }}` in `user_prompt`, and pass real `variables` to the source run. Freeze receives the execution ID, not the future variable values. Later calls pass fresh variables against the same frozen contract. After `get_compilation` reaches initial `status=compiled`, the normal instruction freeze artifact contains `compilation.id` and `instruction_md`. After a frozen test run its projected status can be `completed`; this remains publish-ready and is not a reason to freeze again. It does not generate a standalone source file. Generated `script_code` belongs to the deprecated script compilation path and is not the default production artifact. After freeze, choose one integration branch: - **Owner/private** - call `POST /api/v1/agents/compilations/{compilation_id}/run-instruction/` with a persisted `Idempotency-Key`; use the same key only to replay the identical request. - **Product customers with their own connected service accounts** - publish the same frozen version as one stable deployment, create hosted connection links per required slot, and call `POST /api/v1/agents/deployments/{deployment_id}/run/` with `external_user_id`. `external_user_id` selects that product customer's isolated principal and supported connector bindings. It does not switch the FlyMyAI billing identity: FlyMyAI charges the deployment owner, and the builder applies its own customer billing, quota, credits, or markup. Be precise about the boundary: the normal owner Agents MCP surface can create, run, refine, freeze, and owner-test agents. Deployment publish and customer connect-session remain Agents REST operations. A separately provisioned customer-bound MCP surface can run one already-published deployment for one customer, but only after trusted server configuration binds the deployment, `external_user_id`, and optional mapping. Those authority values are never model-call arguments. Do not confuse customer identity with the OAuth `user_id` of an owner MCP session, and do not invent `client.deployments` calls unless the installed SDK version actually exposes them. If embedded preflight rejects a toolkit, keep the logical worker whole and report the unsupported requirement. Do not silently turn it into two agents or bypass preflight. If a request already says the agent will be embedded in a product or names product customer IDs, take the customer-deployment branch directly. Do not ask whether to split the workflow or create one agent per customer. Every run submission requires an explicit caller-owned retry identity. MCP `run_agent` and `run_frozen` require a nonblank printable ASCII `operation_key` of at most 255 characters with no leading or trailing spaces. REST task runs, compilation runs, and deployment runs require the same constraint in the `Idempotency-Key` header. Neither gateway nor backend generates a fallback. Reserve the key with the canonical request before the first dispatch, reuse it only for the identical request, and use a new key for a new logical run. Leading or trailing spaces, non-ASCII characters, and control characters fail locally. Other authoring writes such as `create_agent`, `freeze_agent`, and deployment creation do not all expose a replay key. Record a unique audit label and the canonical request before dispatch, persist every returned ID immediately, and reconcile a lost response through bounded reads. If the exact outcome is not unique, stop and report the unknown outcome instead of cloning an agent, freeze, or deployment. MCP `create_mcp_resource_set` and `create_agent_group` are durable creates and require their own caller-owned `operation_key`. Their REST collection POSTs require the same value in `Idempotency-Key`. Persist the key with the exact canonical body before dispatch. A retry with the same key and identical body replays the original create response; changed-body reuse returns 409. After a timeout or lost response, retry only the identical body with the original key. Do not generate a replacement key or create a second set or group. Before any connector write, discover the exact action and inspect its schema through the read-only `get_tool_schemas` path. Never call a create, update, send, append, batch-write, or delete action with empty or guessed arguments to learn its schema. If schema discovery returns `not_found`, use the agent runtime for OAuth/Composio actions or stop the direct-call path - do not probe the write. Reserve the caller-owned `operation_key` before every direct external operation and reconcile unknown outcomes instead of dispatching again. Human-readable companion: https://docs.flymy.ai/agents/guides/serverless-agents 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 the search result does not include the full request schema, call `get_tool_schemas` with the returned `runtime_name`. This is the schema discovery call - never execute the target action to discover its fields. 3. If configured and direct execution is supported, call `execute_tool` with exactly `tool`, `action`, `arguments`, and a caller-owned stable `operation_key`. When selecting a non-default instance, also pass its exact configured-tool public UUID as `connection_id`; omission is supported only while one eligible connection is unambiguous. For OAuth/Composio actions, use the agent runtime. 4. If not configured, call `add_tool` with the returned tool slug. The current gateway returns either the exact connection record or guidance text with a hosted `https://app.flymy.ai/mcp-configs#?connection_id=` link for that row. A slug-only link is reserved for a flow with no row yet. After the user finishes there, call `list_configured_tools`, match the exact `public_id` and `alias`, and inspect `is_configured`, `connection_status`, `connection_status_reason`, `connection_status_valid_until`, and `connect_url`. Do not invent a `setup_url` response field. 5. To revoke one exact connection, call `delete_tool` with that row's numeric `id` from `list_configured_tools` after matching its `public_id` and `alias` (REST: `DELETE /api/v1/agents/tools/{id}/`). This is the only revocation operation: it closes local authority and records a durable provider revocation for that connection alone, so sibling connections of the same toolkit keep working and a later dispatch on the revoked row fails before any provider call. 5. Any image, video, audio, music, speech, transcription, or media task - call `recommend_model` or `list_media_models`, inspect schema and price, then `run_model`. 6. 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"}) get_tool_schemas({ "runtime_names":["custom--telegram--telegram_list_dialogs"] }) execute_tool({ "tool":"telegram", "action":"telegram_list_dialogs", "arguments":{"query":"FlyMyAI"}, "operation_key":"telegram-list-dialogs-" }) ``` 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. ## Personal default - one account, no MCP resource-set setup Keep a first-time personal workflow concept-light. Set `mcp_access_mode` to `legacy` for this direct personal projection; switch it explicitly to `scoped` only when granting named resource sets: 1. Start in the user's implicit personal space and call `search_tools` for the capability. 2. If the toolkit is not connected, send the hosted setup link from the gateway guidance or the exact row's `connect_url`; both retain its public connection UUID. A programmatic flow may call `add_tool` without `alias`, which addresses the idempotent `default` connection. 3. Call `list_configured_tools`, match the exact alias and `public_id`, and retain the numeric `id`. Require `is_configured:true`; `connected` is ready, `setup_required` or `reconnect_required` needs the hosted link, `verification_pending` needs a bounded live read, and `temporarily_unavailable` must be reported rather than bypassed. Pass the numeric ID in `available_tools` on one `create_agent` or `update_agent` call. Use `mcp_access_mode:"legacy"` for this direct-attachment projection. Request one bounded page with exact `mcp_tool` and `alias:"default"`, and pass every non-null `next_cursor` back unchanged. 4. Run, refine, freeze, owner-test, and deploy that same agent through the lifecycle above. Do not require the user to create or name resource sets, groups, slots, grants, principals, revisions, or mappings for this first successful flow. The default connection is a safe projection of the same exact persisted identity and authority model used by the advanced path. It can be added to a named set later without rebuilding the agent or reconnecting the account. ## Grow into multiple accounts and MCP resource sets Treat the toolkit, exact authenticated connection, named MCP resource set, agent grant, and runtime binding as different identities. Aliases are human labels only. Authorization, dispatch, refresh, revocation, and freeze use stable public IDs. When one workflow needs several accounts of the same connector: 1. Call `add_tool` once per exact account with a unique `alias`, such as `support`, `sales`, or `archive`. Omitting `alias` addresses only the idempotent legacy alias `default`. 2. Persist each returned numeric `id` and stable `public_id`. The numeric ID is for legacy direct agent attachment; the public UUID is for resource sets and cross-system references. 3. Complete setup for each exact row without putting credentials in chat, then confirm its status with bounded `list_configured_tools` pages filtered by exact `mcp_tool` and, when selecting one row, exact `alias`. 4. Reserve a stable caller-owned `operation_key`, call `create_mcp_resource_set` once for the logical MCP resource set, and retain its `public_id`, `revision`, canonical request, and key. 5. Call `replace_mcp_resource_set_members` with that current revision and the complete desired membership. A 409 stale revision means reload, review, and retry - never overwrite another writer blindly. 6. Pass the resource-set public UUID in `mcp_resource_set_ids` on `create_agent` or `update_agent`. Keep the logical worker whole. Before an update, inspect the live `update_agent` schema from MCP `tools/list`; the current contract requires `agent_id` and accepts the same optional agent fields as `create_agent`. Aliases and resource-set slots use only ASCII letters, digits, underscores, and hyphens. Other punctuation, spaces, and unknown authority fields are invalid; never normalize them into a different persisted identity. One owner may retain at most 25 connection rows for one toolkit, including inactive rows. Replaying an existing alias remains idempotent at the limit. Delete an unused row to free capacity; setting `is_active:false` preserves the exact identity and does not free a slot. Example MCP flow: ```text support = add_tool({"mcp_tool":"gmail","alias":"support"}) sales = add_tool({"mcp_tool":"gmail","alias":"sales"}) resource_set = create_mcp_resource_set({ "name":"Inbox operations", "description":"Support and sales mailboxes", "management_mode":"flymyai", "operation_key":"inbox-operations-create-" }) resource_set = replace_mcp_resource_set_members({ "resource_set_id":resource_set.public_id, "expected_revision":resource_set.revision, "members":[ {"resource_type":"user_mcp_tool","resource_id":support.public_id, "slot":"support_read","allowed_actions":["GMAIL_SEARCH_EMAILS"],"position":0}, {"resource_type":"user_mcp_tool","resource_id":support.public_id, "slot":"support_send","allowed_actions":["GMAIL_SEND_EMAIL"],"position":1}, {"resource_type":"user_mcp_tool","resource_id":sales.public_id, "slot":"sales_mailbox","allowed_actions":[],"position":2} ] }) agent = create_agent({ "name":"Cross-mailbox brief", "user_prompt":"Compare support and sales and return a structured brief.", "mcp_resource_set_ids":[resource_set.public_id], "mcp_access_mode":"scoped" }) ``` When expanding an existing simple agent, first include its current `default` connection `public_id` in the new set if the agent must retain that mailbox. Then replace the legacy direct attachment and grant the set in one partial update: ```text agent = update_agent({ "agent_id":agent.uuid, "available_tools":[], "mcp_resource_set_ids":[resource_set.public_id], "mcp_access_mode":"scoped" }) agent = get_agent({"agent_id":agent.uuid}) ``` An omitted attachment field is unchanged; an explicit empty array clears that relation. Verify that `available_tools` is empty and `mcp_resource_set_ids` contains exactly the intended set. Otherwise the direct connection and set grants form a union, which can preserve unintended owner authority. Clear `available_custom_mcp_servers` too only when those legacy attachments are also being replaced. For an owner resource set, use `management_mode:"flymyai"` and omit `principal_id`. For a customer-managed named mapping, use `management_mode:"customer"` with the exact `principal_id`. A FlyMyAI-managed embedded binding is not a principal resource set: the run omits both `resource_set_id` and `connections`, and saved `ConnectionBinding` rows apply. Never send deprecated `external_principal_id`; the accepted field is `principal_id`. A member `resource_type` is `user_mcp_tool`, `custom_mcp_server`, or `integration_connection`. Owner sets accept only the owner's catalog connections and custom servers. External-principal sets accept only exact integration connections for that principal. A set has at most 100 members; one pooled slot has at most 25 connections and cannot mix toolkit or action ceilings. Member identity is `(resource_type, resource_id, slot)`. The same exact resource may appear in several different slots, and each slot keeps its own `allowed_actions` ceiling. Repeating the same tuple is invalid. One scoped agent execution admits at most 100 MCP resource sets, 500 effective connector bindings, 100 logical slots, 5,000 allowed-action entries, and 1 MiB of UTF-8 allowed-action text. An oversized authority graph fails closed before provider dispatch. Omitting `allowed_actions` or sending `allowed_actions:[]` means unrestricted within the connection's underlying toolkit authority - it does not mean deny all. For least-authority work such as mailbox summaries, send the exact discovered read actions rather than an empty array. Every connection pooled in one slot must use the same resulting ceiling. Use `create_agent_group` when several owner agents need the same set. Agent groups are flat. A child or subagent does not inherit a parent's connector authority unless it is explicitly in the group or has a direct grant. Reserve another stable `operation_key` for the group create and reuse it only with that exact create body after an uncertain response. `list_configured_tools`, `list_mcp_resource_sets`, `list_mcp_resource_set_members`, and `list_agent_groups` return one compact cursor envelope shaped exactly as `{next_cursor, previous_cursor, results}`. Use `page_size` from 1-100, treat `cursor` as an opaque, nonblank printable string of at most 1024 Unicode code points, and pass `next_cursor` unchanged to continue. Resource-set and agent-group list `query` is at most 256 printable characters; the member list accepts only its set ID, page size, and cursor; configured-tool list accepts only exact `mcp_tool` and `alias` filters. Never substitute an offset or assume the first page is complete. `list_mcp_resource_sets` rows are summaries with `member_count` and no nested members. Use `get_mcp_resource_set` for one bounded full set or `list_mcp_resource_set_members` for a large member collection. Both `replace_mcp_resource_set_members` and `update_mcp_resource_set` require the current `expected_revision`; reload after HTTP 409 rather than overwriting another writer. At runtime, FlyMyAI exposes one action schema per toolkit action. If several granted connections can execute that action, its schema requires `_flymyai_connection`. Pass one allowed connection `public_id` from that schema. Never choose by alias, provider email, toolkit slug, resource-set name, or row order. Missing, foreign, revoked, expired, or stale selectors fail before provider dispatch. Outside an agent run, direct `execute_tool` selects a non-default persisted instance only through its exact `connection_id`. Omission is supported only while one eligible connection is unambiguous; it never authorizes first-row or alias-based fallback. Freeze records effective set membership, exact connection IDs, logical slots, action ceilings, and set revisions. Editing a mutable set can affect the next version, but it does not retarget an older immutable version. Adding accounts without rebuilding means keeping the same agent UUID: update that agent, accept a new run, freeze a new immutable version, and publish it through the same stable deployment ID. Existing versions and incompatible customer slot mappings do not change in place. Additional aliases and resource-scope mutations can be feature-gated during a staged rollout. If the backend reports that mutations are not enabled, keep the legacy `default` connection behavior and report the gate. Never emulate multiple accounts with hidden first-row selection. Release maintainers deploy expansion and compatible readers before enabling multi-alias writers or new clients. Before removing compatibility readers or reversing the alias constraint, run the backend's read-only alias downgrade preflight and require a clean result. If it finds non-default aliases or scoped state, keep the compatible backend live and do not rewrite customer mappings to force a rollback. Human-readable details and REST examples: https://docs.flymy.ai/agents/guides/mcp-resource-sets ## 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 mapping ``` `external_user_id` is the builder's stable, opaque, non-secret ID, at most 255 characters, and must not start with the reserved `flymyai-owner-` prefix. 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, named, 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. - Before an MCP-authored agent crosses to deployment REST, call MCP `whoami` and `GET /api/v1/agents/me/` with the builder key. Require the same `user_id`; otherwise stop, because the REST key cannot publish another owner's agent. - Derive `external_user_id` from the authenticated server session. Do not accept an arbitrary client-supplied ID or use an email or secret. - Reserve and persist a stable `Idempotency-Key` with the canonical deployment-run request before dispatch. Reuse it only for the identical request through the documented replay contract. Never automatically repeat a write after a timeout, lost response, or other ambiguous dispatch - persist the unknown outcome and reconcile it first. A replay after the first response returns 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 replay 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, mapping_mode, resource_set_id, resource_set_revision, execution_id, idempotency_key)`, omitting mapping fields only when that mode does not use them. 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. ### Choose the customer mapping mode `external_user_id` selects the external principal only. It does not select an account or mapping. - FlyMyAI-managed binding - omit both `resource_set_id` and `connections`; saved `ConnectionBinding` rows apply. - Customer-managed named mapping - store a principal resource-set `public_id` and `revision` in the builder backend, then send both `resource_set_id` and `resource_set_revision` on every run. The backend still accepts an omitted revision for compatibility, but the safe public workflow never omits it. - One-off explicit mapping - send `connections` keyed by frozen slot. `resource_set_id` and `connections` are mutually exclusive. A named mapping can contain only exact `integration_connection` public UUIDs owned by the resolved principal. FlyMyAI revalidates authority, slots, status, and revision at the snapshot boundary. The opaque mapping ID never exposes credentials or changes the deployment owner's billing identity. The exact one-off shape maps each frozen slot to either one connection UUID or an array of connection UUIDs: ```json { "external_user_id":"customer_42", "variables":{}, "connections":{ "support_mailbox":"11111111-1111-4111-8111-111111111111", "compliance_mailbox":["22222222-2222-4222-8222-222222222222"] } } ``` Every UUID must be an active `integration_connection` owned by the resolved principal. Unknown slots, duplicate UUIDs, the wrong toolkit, cross-principal IDs, and cardinality violations fail before provider dispatch. Omitted slots currently continue through saved bindings, so include every required frozen slot when the canonical request must own the complete mapping. Owner `user_mcp_tool` IDs are never valid here, and a customer principal never falls back to the owner's authoring connection. ### Optional customer-bound MCP runtime Use the owner MCP surface to author, test, and freeze. Use Agents REST to publish and bootstrap connections. After that, a product backend may expose a published deployment through a separately provisioned customer MCP process. Current safe customer mode has one immutable deployment/customer binding per gateway process: ```bash FLYMYAI_API_KEY='' \ MCP_HTTP_TOKEN='' \ FLYMYAI_MCP_MODE=customer \ FLYMYAI_MCP_DEPLOYMENT_ID="$DEPLOYMENT_ID" \ FLYMYAI_MCP_EXTERNAL_USER_ID="$AUTHENTICATED_PRODUCT_USER_ID" \ FLYMYAI_MCP_RESOURCE_SET_ID="$CUSTOMER_RESOURCE_SET_ID" \ FLYMYAI_MCP_RESOURCE_SET_REVISION="$CUSTOMER_RESOURCE_SET_REVISION" \ npm start ``` The product backend derives `external_user_id` from its authenticated session and places it in trusted process provisioning. It is never a model-call argument, client header, query parameter, or value copied from chat. Use the resource-set pair for a customer-managed named mapping, omit both resource-set variables for saved FlyMyAI bindings, or set `FLYMYAI_MCP_CONNECTIONS_JSON` for one exact process-managed mapping. Connect the customer-facing MCP client with `Authorization: Bearer `, not the owner API key. The surface exposes only: ```text started = run_bound_deployment({ "variables":{}, "operation_key":"customer-logical-run-" }) page = get_bound_deployment_run({"run_handle":started.run_handle}) ``` Pass `next_since` while `has_more=true` and stop only when `poll_complete=true`. The opaque handle is bound to the configured deployment, customer, and owner credential. To serve another product user, route to a separately trusted binding. Two bindings may use the same deployment, but they must produce different `ExternalPrincipal` and execution records while keeping the same agent, version, deployment, and billed owner. ### Release-maintainer-only connectionless fixture This deterministic platform fixture is not part of an ordinary user's agent lifecycle. A release maintainer may run its one separately labeled disposable fixture agent once against an assembled candidate. An assistant already building a requested worker must skip this entire section and publish that same requested agent and compilation - never create a smoke agent beside it. The separate fresh-assistant gate also creates exactly one requested evaluation agent and must not add this fixture. 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 : "${FLYMYAI_AGENTS_API_ROOT:?Set the exact Agents API root ending in /api/v1/agents}" A="${FLYMYAI_AGENTS_API_ROOT%/}" auth=(-H "X-API-KEY: $FLYMYAI_API_KEY") : "${OPERATION_LABEL:?Persist a unique audit label before any write}" : "${SOURCE_RUN_KEY:?Reserve a caller-owned key for the source run}" 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" --arg label "$OPERATION_LABEL" \ '{name:("Embedded Hacker News smoke ["+$label+"]"),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 "Idempotency-Key: $SOURCE_RUN_KEY" \ -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 freeze=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \ "$A/compilations/freeze-instruction/$EXECUTION_ID/") COMPILATION_ID=$(jq -er '.id' <<<"$freeze") ``` The final POST freezes this fixture's accepted `EXECUTION_ID` exactly once. Persist the returned `COMPILATION_ID` immediately and continue with the universal publish sequence below. If its response is lost, reconcile by the known execution ID rather than repeating it. Before publish, this fixture's 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 `AGENT_ID` and `COMPILATION_ID` must identify the one requested agent and its one accepted instruction freeze, or the one disposable fixture agent only when a release maintainer deliberately ran the preceding platform fixture. Never freeze the same accepted execution again merely to enter this publish sequence. `FLYMYAI_API_KEY` must already be exported by the builder's secret manager. Do not edit the agent between the accepted freeze and version selection. If a freeze response was lost, do not POST it again: reconcile with the bounded compilation list filtered by the known execution ID and stop if the outcome is not unique. ```bash set -euo pipefail : "${FLYMYAI_API_KEY:?Export the builder key in this server process}" : "${AGENT_ID:?Set from create_agent}" : "${COMPILATION_ID:?Set from the one accepted instruction freeze}" : "${OPERATION_LABEL:?Reuse the unique audit label from agent creation}" : "${FLYMYAI_AGENTS_API_ROOT:?Set the exact Agents API root ending in /api/v1/agents}" A="${FLYMYAI_AGENTS_API_ROOT%/}" auth=(-H "X-API-KEY: $FLYMYAI_API_KEY") for attempt in $(seq 1 1350); do compilation=$(curl -fsS --max-time 30 "${auth[@]}" \ "$A/compilations/$COMPILATION_ID/") status=$(jq -r '.status' <<<"$compilation") error=$(jq -r '.error // empty' <<<"$compilation") if test -n "$error"; then jq '{status,error}' <<<"$compilation"; exit 1; fi case "$status" in compiled|completed) break ;; failed) jq '{status,error}' <<<"$compilation"; exit 1 ;; esac sleep 2 done case "$status" in compiled|completed) ;; *) exit 1 ;; esac VERSION_ID= for attempt in $(seq 1 1350); do next_url="$A/versions/?agent_task=$AGENT_ID" version_ids='[]' for page in $(seq 1 100); do case "$next_url" in "$A/versions/"*) ;; *) exit 1 ;; esac versions=$(curl -fsS --max-time 30 "${auth[@]}" "$next_url") page_ids=$(jq -c --argjson c "$COMPILATION_ID" \ '[.results[]|select(.source_compilation==$c)|.public_id]' <<<"$versions") version_ids=$(jq -cn --argjson prior "$version_ids" \ --argjson current "$page_ids" '$prior+$current') next_url=$(jq -r '.next // empty' <<<"$versions") if test -z "$next_url"; then break; fi if test "$page" = 100; then exit 1; fi done case "$(jq -r 'length' <<<"$version_ids")" in 0) sleep 2 ;; 1) VERSION_ID=$(jq -r '.[0]' <<<"$version_ids"); break ;; *) echo "Multiple versions materialized for compilation $COMPILATION_ID" >&2; exit 1 ;; esac done test -n "$VERSION_ID" # The versions route is cursor-paginated. The loop follows at most 100 # same-origin pages and never sends the builder key to an untrusted next URL. 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" --arg label "$OPERATION_LABEL" \ '{agent_task:$a,candidate_version:$v,name:("Production ["+$label+"]"),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 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. Upgrade and rollback that same deployment - never create a deployment per release. PATCH its `candidate_version`, run preflight, then publish. To roll back, stage the prior immutable version on the same ID and repeat the same two checks: ```bash curl -fsS --max-time 30 -X PATCH "${auth[@]}" \ -H 'Content-Type: application/json' \ --data "$(jq -nc --arg v "$NEXT_VERSION_ID" '{candidate_version:$v}')" \ "$A/deployments/$DEPLOYMENT_ID/" 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/" >/dev/null ``` 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`: The deployment `access` GET is read-only. It can return requirements and existing customer state, but it never creates an `ExternalPrincipal`. For a new external user, the `connect-session` POST below is the first mutating bootstrap: it resolves or creates the principal and then creates the hosted authorization session. ```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: Treat `connect-session` as a single-dispatch write. Persist its canonical deployment, customer, slot, and audit label before calling. It has no caller idempotency key. After a timeout or lost response, do not immediately create a second session; reconcile through `access` and the known 15-minute window, and surface an unknown outcome if uniqueness cannot be proven. ```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,principal,toolkit_slug,alias,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. Keep the full `access` response on the authenticated builder backend. Send only the hosted `redirect_url` to the customer, not owner policy, connection, or binding metadata. #### Optional customer-managed named mapping After the exact hosted connections are active, resolve the principal and create one principal-scoped resource set. Use exact active connection `public_id` values from that customer's `access` response: ```bash : "${CUSTOMER_CONNECTION_ID:?Exact active connection public_id from access}" : "${CUSTOMER_RESOURCE_SET_CREATE_KEY:?Persist one key for this exact customer mapping create}" principals=$(curl -fsS --max-time 30 -G "${auth[@]}" \ --data-urlencode "deployment=$DEPLOYMENT_ID" \ --data-urlencode "external_user_id=$EXTERNAL_USER_ID" \ "$A/external-principals/") PRINCIPAL_ID=$(jq -er ' (.results // .) as $rows | if ($rows|length)==1 then $rows[0].public_id else error("expected exactly one external principal") end ' <<<"$principals") mapping=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \ -H "Idempotency-Key: $CUSTOMER_RESOURCE_SET_CREATE_KEY" \ -H 'Content-Type: application/json' \ --data "$(jq -nc --arg p "$PRINCIPAL_ID" '{ name:"Primary customer mapping", management_mode:"customer", principal_id:$p }')" \ "$A/mcp-resource-sets/") CUSTOMER_RESOURCE_SET_ID=$(jq -er '.public_id' <<<"$mapping") CUSTOMER_RESOURCE_SET_REVISION=$(jq -er '.revision' <<<"$mapping") mapping=$(curl -fsS --max-time 30 -X POST "${auth[@]}" \ -H 'Content-Type: application/json' \ --data "$(jq -nc \ --arg connection "$CUSTOMER_CONNECTION_ID" \ --arg slot "$SLOT" \ --argjson revision "$CUSTOMER_RESOURCE_SET_REVISION" '{ expected_revision:$revision, members:[{ resource_type:"integration_connection", resource_id:$connection, slot:$slot, allowed_actions:[], position:0 }] }')" \ "$A/mcp-resource-sets/$CUSTOMER_RESOURCE_SET_ID/replace-members/") CUSTOMER_RESOURCE_SET_REVISION=$(jq -er '.revision' <<<"$mapping") ``` For several slots or accounts, send the complete desired member array in one replacement. A set can reference only connections owned by that exact principal. Persist the returned mapping ID and revision in the builder backend. ### Run and meter one user This released HTTP contract keeps the key server-side and uses the slim progress route. Store the canonical variables object alongside the idempotency key; `AGENT_VARIABLES_JSON={}` is valid only when the frozen input schema has no required fields: ```python import json, os, time, uuid import httpx base = os.environ["FLYMYAI_AGENTS_API_ROOT"].rstrip("/") if not base.endswith("/api/v1/agents"): raise ValueError("FLYMYAI_AGENTS_API_ROOT must end in /api/v1/agents") deployment = os.environ["FLYMYAI_DEPLOYMENT_ID"] user_id = os.environ["PRODUCT_USER_ID"] idem = os.environ["PERSISTED_DEPLOYMENT_RUN_KEY"] variables = json.loads(os.environ.get("AGENT_VARIABLES_JSON", "{}")) if not isinstance(variables, dict): raise ValueError("AGENT_VARIABLES_JSON must contain a JSON object") request_body = {"external_user_id": user_id, "variables": variables} mapping_id = os.environ.get("CUSTOMER_MCP_RESOURCE_SET_ID") mapping_revision = os.environ.get("CUSTOMER_MCP_RESOURCE_SET_REVISION") connections_json = os.environ.get("CUSTOMER_CONNECTIONS_JSON") if mapping_id and connections_json: raise ValueError("Choose resource_set_id or connections, not both") if mapping_revision and not mapping_id: raise ValueError("A resource-set revision requires its mapping id") if mapping_id: if not mapping_revision: raise ValueError("The public named-mapping workflow requires its revision") request_body["resource_set_id"] = mapping_id request_body["resource_set_revision"] = int(mapping_revision) if request_body["resource_set_revision"] < 1: raise ValueError("The mapping revision must be positive") elif connections_json: connections = json.loads(connections_json) if not isinstance(connections, dict): raise ValueError("CUSTOMER_CONNECTIONS_JSON must contain a JSON object") if len(connections) > 100: raise ValueError("CUSTOMER_CONNECTIONS_JSON exceeds 100 slots") for slot, selected in connections.items(): if not isinstance(slot, str) or not slot: raise ValueError("Every connection mapping key must be a frozen slot") ids = [selected] if isinstance(selected, str) else selected if not isinstance(ids, list) or not ids or len(ids) > 25: raise ValueError(f"Slot {slot} must select 1-25 connection UUIDs") if any(not isinstance(value, str) for value in ids): raise ValueError(f"Slot {slot} connection IDs must be strings") for value in ids: uuid.UUID(value) request_body["connections"] = connections 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=request_body) response.raise_for_status() execution = response.json()["id"] retry = client.post(f"{base}/deployments/{deployment}/run/", headers={"Idempotency-Key": idem}, json=request_body) 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. ### Early release gate - one deployment, two customers Run two independent blocking gates against the release candidate before broad regression suites and before production promotion: 1. A deterministic API gate validates the exact public release tuple and guide before mutation, then creates one labeled agent, performs one accepted live run, one real instruction freeze, one immutable version, and one deployment. It calls the deployment for two synthetic `external_user_id` values and proves idempotent replay. 2. A separate fresh-assistant evaluation starts in an empty directory with only those exact guide bytes as FlyMyAI documentation. Give it a normal embedded-product request without lifecycle, customer-ID, or split hints. Grade structured MCP tool calls. After it publishes one deployment, the trusted harness exercises two customer bindings and independently verifies every ID over REST; prose claiming that calls happened is not evidence. These are release-maintainer gates, not extra agents in a user's lifecycle. The deterministic gate owns its one disposable fixture agent. The fresh assistant owns exactly one requested evaluation agent. It must not also run the fixture section above. Before either gate, require all candidate coordinates: ```bash : "${CANDIDATE_SKILL_URL:?Candidate guide URL is required}" : "${CANDIDATE_RELEASE_CONTRACT_URL:?Candidate release contract URL is required}" : "${CANDIDATE_RELEASE_TUPLE_SHA256:?Approved candidate tuple hash is required}" : "${CANDIDATE_MCP_URL:?Candidate MCP URL is required}" : "${CANDIDATE_AGENTS_API_ROOT:?Candidate Agents API root is required}" : "${CANDIDATE_OPENAPI_URL:?Candidate OpenAPI URL is required}" case "$CANDIDATE_AGENTS_API_ROOT" in */api/v1/agents) ;; *) exit 1 ;; esac export FLYMYAI_AGENTS_API_ROOT="$CANDIDATE_AGENTS_API_ROOT" ``` The product tuple excludes the two benchmark result attestations so they can move from pending to passed without a self-referential hash. After both gates pass, pin the exact ready manifest bytes for the final read-only promotion check. A changed product tuple always requires both lifecycle gates again. Do not substitute the production roots when any candidate coordinate is missing. The trusted release harness must mint and persist the two synthetic customer IDs in its own authenticated test identity store before any request. The assistant receives the product task, not authority to choose those IDs. The owner Agents MCP remains an authoring surface and does not gain arbitrary customer administration or a model-supplied `external_user_id`. Deployment publish, principal setup, customer connection, and customer execution remain typed REST and SDK operations performed by the trusted harness. A separately provisioned customer-bound MCP exposes only its fixed deployment/customer binding. For an MCP resource-scope release, add a blocking scope contract before the general A/B topology checks. It must create several same-toolkit connection fixtures with distinct aliases and public IDs, place them in one revisioned owner set, grant that set directly and through one flat agent group, require an exact selector under ambiguity, and prove a revoke fails before provider dispatch. It must also create one external-principal named mapping and reject a foreign principal, stale revision, and a body that supplies both `resource_set_id` and `connections`. Run the real provider OAuth acceptance test separately after interactive authorization. For both harness-owned IDs, require exactly one result from `GET /external-principals/?deployment=&external_user_id=` and different principal `public_id` values. For each customer execution, require exactly one `GET /runtime-snapshots/?execution=` result. Both snapshots must contain the same deployment and immutable `agent_version`, and each snapshot's `billing_user` must equal `GET /me/` field `user_id`, while execution and principal differ. Replay each identical deployment request with its original `Idempotency-Key` and require the original execution ID. Read both prices with the same builder key. Query deployment `access` separately for both external IDs and reject any connection or binding UUID that crosses principals. Use a connectionless supported adapter for this deterministic topology gate, then test each real provider separately. Preserve the exact prompt, guide bytes and SHA-256, structured transcript, operation ledger, created IDs, REST evidence, and cleanup result. Cleanup only labeled test resources: revoke any test connections, disable both principals, archive the deployment, then soft-archive the agent. Immutable versions, snapshots, prices, and other audit rows remain. Connection revocation can intentionally leave a revoked connection audit row and an empty binding row; require terminal revoked/unbound state, not physical deletion. A missing principal, one shared principal, a changed deployment/version, an idempotency replay that creates a new execution, owner-billing drift, incomplete cleanup, or a task status patch standing in for instruction freeze blocks release. This candidate guide is aligned to `flymyai==1.2.0rc4`. That candidate exposes `client.versions`, `client.deployments`, `client.mcp_resource_sets`, and `client.agent_groups` in both sync and async clients. Capability-detect the installed SDK before using those namespaces. An older installed package may require the equivalent REST routes; never invent a namespace that is absent. ## Personal tools and MCP The released production coordinates are: ```bash export FLYMYAI_MCP_URL=https://mcp-agents.flymy.ai/mcp export FLYMYAI_AGENTS_API_ROOT=https://backend.flymy.ai/api/v1/agents ``` Do not use those defaults for a release-candidate gate. Compatible GUI clients can sign in to the intended MCP environment. Configuration-driven clients can send `X-API-Key` from their secret manager: ```bash claude mcp add --transport http flymyai "$FLYMYAI_MCP_URL" \ --header "X-API-Key: $FLYMYAI_API_KEY" ``` If FlyMyAI is already connected, do not reinstall it. Call `whoami`, `tools/list`, `search_tools`, or `recommend_model`. If `tools/list` is available, inspect the live schema before lifecycle mutations such as `update_agent`; do not infer parameters from prose alone. `list_agents` is cursor-paginated on the current gateway. Request one bounded page with `page_size` from 1 to 100, then pass its opaque `next_cursor` back as `cursor` until it is null. Never replace a cursor with an offset or download an account-wide list to reconcile one labeled write. 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}}' \ "$FLYMYAI_AGENTS_API_ROOT/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`, exact `arguments`, and a caller-owned stable `operation_key`; the gateway forwards it as `Idempotency-Key`. Reuse that key only for the identical canonical request and reconcile an ambiguous outcome instead of automatically dispatching again. 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. Candidate SDK image call through the unchanged Models API: ```bash python -m pip install 'flymyai==1.2.0rc4' ``` ```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.", "mcp_access_mode":"legacy", "output_schema":{"type":"object","properties":{"status":{"type":"string"}}, "required":["status"],"additionalProperties":false} }) run = run_agent({ "agent_id":agent.uuid, "variables":{}, "operation_key":"health-source-run-" }) run_page = get_run({"execution_id":run.id}) # Continue with since=run_page.next_since while run_page.poll_complete is false. frozen = freeze_agent({"execution_id":run.id}) compilation = get_compilation({"compilation_id":frozen.id}) # Poll until compilation.status is compiled or failed. test = run_frozen({ "compilation_id":frozen.id, "operation_key":"health-frozen-test-" }) test_page = get_run({"execution_id":test.id}) # Poll until test_page.poll_complete is true, then verify the bounded result. ``` Runs are asynchronous. Poll `get_run` with `since` equal to the previous `next_since` while `poll_complete=false`. Freeze only an accepted completed run, poll `get_compilation` until `compiled`, and test the frozen compilation before handing off integration code. 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":{}}' \ "$FLYMYAI_AGENTS_API_ROOT/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.