For AI agents: Documentation index at /llms.txt

Skip to content

Service discoverability

When an AI agent is handed only your app’s URL (for example, https://yourapp.com), it should be able to work out the rest on its own: which canisters your app comprises, what each one does, how to call them, how to query their data, and how to act as the signed-in user. No human supplying canister IDs, no bespoke integration.

This guide describes what a canister app exposes to make that possible, ordered by priority.

An agent handed only your app’s URL should be able to do five things, unattended:

  1. Enumerate every canister the app comprises, and each one’s role.
  2. Inspect each canister’s typed interface.
  3. Understand the behavior the types cannot convey.
  4. Query the app’s data efficiently, without a bespoke method per question.
  5. Act as the signed-in user, with that user’s own permissions.

Each layer is independently adoptable and independently useful. Together they make an app agent-ready.

LayerQuestion it answersMechanism
1. CompositionWhich canisters make up this app, and what is each for?/.well-known/ic-architecture manifest
2. InterfaceWhat methods and types does a canister expose?candid:service metadata
3. BehaviorHow does it actually behave (units, lifecycle, gotchas)?getApiDoc query method
4. DataHow do I query its data?OQL: schema and execute query methods
5. IdentityHow do I act as the signed-in user, under the right principal?/.well-known/ii-derivation-origin declaration

An app should declare the set of canisters it comprises, each labeled with its role.

Serve a JSON document at the origin’s /.well-known/ic-architecture that lists every canister and its role:

{
"version": "1.0.0",
"canisters": [
{
"id": "hcv4s-uaaaa-aaabq-qaaba-cai",
"name": "frontend",
"role": "the frontend"
},
{
"id": "hmxr2-pqaaa-aaabq-qaaaa-cai",
"name": "backend",
"role": "the backend",
"description": "orders + inventory API; call getApiDoc() first"
}
]
}

This is the way an app declares its composition. It is recommended to create this file during your app’s deployment, as opposed to updating it for an already-deployed app, as demonstrated here.

Field rules:

  • version identifies the manifest schema version.
  • id is required and must be a canister principal.
  • name and role label the canister, and description is optional. These human-readable fields are untrusted, so a consumer sanitizes them before use.
  • Unknown fields must be ignored, so the format can grow (for example, per-canister network hints or an api-doc pointer) without breaking older readers.

Serving rules:

  • Serve it at exactly /.well-known/ic-architecture, at the origin, with no file extension. The IC’s .well-known discovery files omit extensions by convention (compare ic-domains and ii-alternative-origins), even when, as here, the content is JSON.
  • Serve real JSON with Content-Type: application/json. The most common failure is a single-page-app catch-all returning index.html for unknown paths. Exempt /.well-known/* from the SPA rewrite wherever your frontend is served.
  • Generate it at deploy time. Canister IDs differ per network (local, staging, mainnet), so the file must be produced by the deploy pipeline (which already knows the IDs) rather than committed with hard-coded values.

The exact configuration depends on how you host the frontend; the requirement is only that /.well-known/* is served as a static file, not rewritten to index.html. If you serve assets from an asset canister, see Asset canister for including the hidden .well-known directory and configuring SPA aliasing, and Custom domains for the same .well-known pattern applied to domain ownership.

Expose your Candid interface as the canister’s public candid:service metadata, the standard IC mechanism emitted by default by the common toolchains. This lets an agent fetch the exact method signatures and types and encode or decode calls correctly.

See Candid interface for how Candid describes a canister’s methods and types.

Candid types describe shape, not behavior. Expose a query method that returns a prose (markdown) guide to the things an agent cannot infer from types:

getApiDoc : () -> (text) query; // or the snake_case name get_api_doc

Cover the non-obvious semantics, for example:

  • Units and encoding: integer money scaled by 10^8, fractions versus tenth-bps, timestamp units.
  • Authentication: which calls need a signed principal, and how anonymous access differs from a signed-in user.
  • Lifecycle: staged or asynchronous operations that return before completing, so the agent must poll.
  • Mutation safety: what is irreversible, and any dead-man switches.
  • Polling rules and the gotchas that routinely trip up new integrators.

Name it discoverably. Because the method name itself appears in candid:service, an agent finds getApiDoc with zero out-of-band knowledge: no bootstrap hint, meta tag, or side channel required.

For data-rich apps, expose a self-describing query surface so an agent can answer questions without you writing a bespoke method per question. OQL is one such convention, a pair of query methods:

schema : () -> (text) query; // JSON catalogue: entities, fields, edges
execute : (text) -> (Result) query; // one JSON query object -> rows

schema returns a JSON catalogue of entities, their fields (with types and roles), and the edges between them. An agent fetches it once so it knows what is queryable.

execute takes one JSON query object (filters, aggregation, ordering, projection, paging) and returns a paged Result:

type Cell = record { name : text; value : variant { ... } }; // value tagged by its scalar type
type Result = record { hasMore : bool; rows : vec vec Cell }; // each row is a list of named cells

Each cell carries its column name and a value that is a type-tagged variant (text, integer, and so on), so agents read cells by name, never by position, and page while hasMore is true. Prefer server-side filtering and aggregation so only the needed data crosses into the agent’s context. Any Candid interface works; OQL just makes open-ended questions more economical.

To let an agent act with the user’s own principal and permissions, an app should expose the Internet Identity derivation origin its frontends pin. An agent that already holds the user’s Internet Identity authorization derives a short-lived, per-app delegation for that origin on demand. This yields the same principal the user has when they use your app in a web browser, so your existing access control applies unchanged.

The principal a user gets is a function of three inputs:

  1. The user’s Internet Identity
  2. The account within that Internet Identity
  3. Your app’s derivation origin (the only factor controlled by your app)

The derivation origin defaults to the visible origin requested by the user (for agentic flows) or the origin a user sees in their web browser address line (for classical flows).

If the app has multiple frontends (e.g., due to migrating to a new brand name) the visible URL is not necessarily the origin identities are derived for. Providing the well-known file below tells an agent which origin to request Internet Identity derivations for when your users prompt that agent to access the app from any of its supported origins (e.g., starting from a new or secondary frontend).

Instructions. Each of the frontend origins your app supports should publish the app’s derivation origin in a dedicated file at /.well-known/ii-derivation-origin, whose body is the canonical https://host origin on a single line:

https://hcv4s-uaaaa-aaabq-qaaba-cai.icp.net

If you use the default (the app’s own origin), you may omit the file. Its absence means “derive for the visible / requested origin itself.” Serve it with no file extension and exempt /.well-known/* from the SPA catch-all, exactly as for the manifest. Generate it at deploy time when the origin is a per-network canister URL.

Relationship between derivation origin and alternative-origins. A custom origin is enabled by two coupled files: the app pins derivationOrigin in its Internet Identity configuration, and the derivation origin publishes /.well-known/ii-alternative-origins listing the origins permitted to derive against it. That list answers “who may point here,” not “where does this app point.” The two are not interchangeable, and there is no reverse lookup from an app URL to its custom derivation origin. Reading it the wrong way round silently produces the wrong principal. See Internet Identity for how to configure derivationOrigin and ii-alternative-origins.

  • Composition: the deploy pipeline emits /.well-known/ic-architecture listing every canister with a role, served as real JSON at the extensionless path.
  • Routing: /.well-known/* is exempt from the SPA catch-all rewrite.
  • Interface: candid:service metadata is exposed (do not strip it).
  • Behavior: the backend exposes getApiDoc or get_api_doc, returning a markdown guide.
  • Data (if applicable): data-rich canisters expose OQL schema and execute.
  • Identity (if custom): publish the effective origin in /.well-known/ii-derivation-origin (canonical https://host, one line).

An app is agent-discoverable when these pass against the deployed origin:

Terminal window
# 1. Manifest is real JSON listing the canisters (not the SPA shell)
curl -s https://APP/.well-known/ic-architecture | jq '.canisters[].id'
# 2. Backend exposes candid:service; fetch it against the backend ID from step 1
# (and confirm the interface declares getApiDoc, plus schema/execute if data-rich)
icp canister metadata <BACKEND_ID> candid:service -e ic
# 3. If you pin a CUSTOM derivation origin, it is published in its own file as the
# canonical https://host. An absent file means the default (https://APP).
# Use -f so a 404 is treated as an error and the fallback fires (curl -s alone
# exits 0 on 404, so the "default" branch would never run).
curl -sf https://APP/.well-known/ii-derivation-origin || echo "default (https://APP)"

End to end: an agent given only https://APP resolves the backend ID first (labeled with its role), reads getApiDoc to learn behavior, queries data apps via OQL, and, to act as the user, derives the user’s principal against the app’s declared derivation origin. All of that happens without a human supplying an ID or guessing which origin the user’s principal comes from.