Skip to main content

Embedded and resale builder reference

This page holds the complete builder sequence from the public agent guide at flymy.ai/skill.md, which keeps only its essence. The step-by-step walkthrough is Create, Freeze & Embed, the customer identity model is Per-customer MCP Access, and named customer mappings are covered in Multiple MCP Accounts & Resource Sets.

Deployment runs require a nonblank printable ASCII Idempotency-Key of 1-255 characters with no leading or trailing spaces. Neither gateway nor backend generates a fallback: reserve the key with the canonical request before the first dispatch and reuse it only for the identical request. Leading or trailing spaces, non-ASCII characters, and control characters fail locally.

Choose the mode​

ModeFlyMyAI identity and keyService connectionsBilling
PersonalThe account connected to this assistantThat account's servicesThat FlyMyAI account
Embedded or resaleOne builder key on the builder backendEach product user's own servicesFlyMyAI charges the builder; the builder charges its users

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

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.

For the owner/private branch, the exact REST schedule update for an existing compilation is:

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":""}.

One builder key, no end-user keys​

The builder wires FlyMyAI once:

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:

{
"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:

FLYMYAI_API_KEY='<server-owner-key>' \
MCP_HTTP_TOKEN='<customer-edge-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 <customer-edge-token>, not the owner API key. The surface exposes only:

started = run_bound_deployment({
"variables":{},
"operation_key":"customer-logical-run-<uuid>"
})
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.

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 connectionless 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.

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:

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.

: "${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.

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:

: "${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:

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.

SDK namespaces​

This candidate guide is aligned to flymyai==1.2.0rc5. 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.