From 1aaad4cb72cb7edd4e5d9e96dde3ce18bde4283a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 22:15:15 +0200 Subject: [PATCH 1/4] feat: adopt icp.net as the mainnet HTTP gateway domain icp-cli v1.0.0 changed the default mainnet HTTP gateway from icp0.io to icp.net (browser canister access is now .icp.net / raw.icp.net). Reconcile the skills with the developer-docs change. Changed (gateway / browser canister-access URLs): - asset-canister: mainnet access URL, certified/raw serving domains, browser-open URL; also fix the Node.js HttpAgent host from the legacy ic0.app access domain to the canonical API endpoint icp-api.io - custom-domains: default access URL, ic-domains fetch URLs, and the custom-domains/v1 REST API host (confirmed to resolve on icp.net) - certified-variables: mainnet curl example - deploy-to-cloud-engine: frontend canister access URL - internet-identity: illustrative frontend_origins / TRUSTED_ORIGIN Kept (NOT the gateway): - API/agent hosts stay icp-api.io (icp.net is gateway-only) - Named dapp domains (nns.ic0.app) in icp-cli and agent-web-identity - II delegation rewriting pitfall: II still rewrites icp0.io -> ic0.app at the protocol level (per the II spec, unchanged by the gateway rename), so that behavioral claim stays as-is rather than becoming a meaningless icp.net-vs-icp.net statement Evals: updated custom-domains API-endpoint and asset-canister host assertions; added an asset-canister guard that the mainnet browser URL is .icp.net (not ic0.app/icp0.io/icp-api.io). Both new/updated cases pass with the skill. Closes #224 --- evaluations/asset-canister.json | 11 +++++++++- evaluations/custom-domains.json | 12 +++++------ skills/asset-canister/SKILL.md | 8 +++---- skills/certified-variables/SKILL.md | 2 +- skills/custom-domains/SKILL.md | 30 +++++++++++++------------- skills/deploy-to-cloud-engine/SKILL.md | 2 +- skills/internet-identity/SKILL.md | 4 ++-- 7 files changed, 39 insertions(+), 30 deletions(-) diff --git a/evaluations/asset-canister.json b/evaluations/asset-canister.json index fea19c7..48cc41a 100644 --- a/evaluations/asset-canister.json +++ b/evaluations/asset-canister.json @@ -3,6 +3,15 @@ "description": "Evaluation cases for the asset-canister skill. Tests whether agents produce correct asset canister configuration, avoid top-level await in programmatic upload code, and handle SPA routing.", "output_evals": [ + { + "name": "Mainnet browser-access domain is icp.net", + "prompt": "I deployed my asset (frontend) canister to mainnet. What URL do I open in the browser to view it? Just the URL pattern.", + "expected_behaviors": [ + "Gives the URL as https://.icp.net", + "Does NOT use the legacy ic0.app or icp0.io gateway domains", + "Does NOT use icp-api.io (that is the API endpoint, not the browser gateway)" + ] + }, { "name": "Programmatic asset upload", "prompt": "Show me a Node.js script to create an HttpAgent and AssetManager, then upload a single file to my asset canister on the local replica. Keep it minimal — no icp.yaml, no deploy steps.", @@ -49,7 +58,7 @@ "expected_behaviors": [ "Sets shouldFetchRootKey to true ONLY for local development", "Explicitly warns that shouldFetchRootKey on mainnet is a security vulnerability", - "Uses different host values for local (localhost:8000) vs mainnet (ic0.app or icp-api.io)" + "Uses different host values for local (localhost:8000) vs mainnet (icp-api.io)" ] } ], diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json index 813668f..c6c6ea8 100644 --- a/evaluations/custom-domains.json +++ b/evaluations/custom-domains.json @@ -17,9 +17,9 @@ "name": "Domain registration API", "prompt": "I've set up my DNS records and deployed my canister with the ic-domains file. What curl commands do I run to register my custom domain app.example.com? Just the commands.", "expected_behaviors": [ - "Uses the validate endpoint: GET https://icp0.io/custom-domains/v1/app.example.com/validate", - "Uses the registration endpoint: POST https://icp0.io/custom-domains/v1/app.example.com", - "Uses the status check endpoint: GET https://icp0.io/custom-domains/v1/app.example.com", + "Uses the validate endpoint: GET https://icp.net/custom-domains/v1/app.example.com/validate", + "Uses the registration endpoint: POST https://icp.net/custom-domains/v1/app.example.com", + "Uses the status check endpoint: GET https://icp.net/custom-domains/v1/app.example.com", "Does NOT invent non-existent API endpoints or parameters" ] }, @@ -53,7 +53,7 @@ }, { "name": "HttpAgent host configuration", - "prompt": "My IC frontend works fine on icp0.io but API calls fail when I access it through my custom domain. What's wrong? Just the fix.", + "prompt": "My IC frontend works fine on icp.net but API calls fail when I access it through my custom domain. What's wrong? Just the fix.", "expected_behaviors": [ "Identifies that HttpAgent cannot auto-detect the IC API host on custom domains", "Shows setting host to https://icp-api.io for mainnet", @@ -65,7 +65,7 @@ "prompt": "I want to point my existing custom domain to a different canister. What do I need to do? Just the steps.", "expected_behaviors": [ "Updates the _canister-id TXT record to the new canister ID", - "Uses PATCH https://icp0.io/custom-domains/v1/DOMAIN to notify the service", + "Uses PATCH https://icp.net/custom-domains/v1/DOMAIN to notify the service", "Does NOT say to delete and re-register the domain" ] }, @@ -74,7 +74,7 @@ "prompt": "How do I remove a custom domain registration from the IC? Just the steps.", "expected_behaviors": [ "Removes the _canister-id TXT and _acme-challenge CNAME DNS records", - "Uses DELETE https://icp0.io/custom-domains/v1/DOMAIN to notify the service", + "Uses DELETE https://icp.net/custom-domains/v1/DOMAIN to notify the service", "Does NOT suggest only removing DNS records without calling the API" ] } diff --git a/skills/asset-canister/SKILL.md b/skills/asset-canister/SKILL.md index 7a2aab8..4f37cd0 100644 --- a/skills/asset-canister/SKILL.md +++ b/skills/asset-canister/SKILL.md @@ -26,7 +26,7 @@ Access patterns: | Environment | URL Pattern | |-------------|-------------| | Local | `http://.localhost:8000` | -| Mainnet | `https://.ic0.app` or `https://.icp0.io` | +| Mainnet | `https://.icp.net` | | Custom domain | `https://yourdomain.com` (with DNS configuration) | ## Mistakes That Break Your Build @@ -45,7 +45,7 @@ Access patterns: 7. **Pinning the asset canister Wasm version below `0.30.2`.** The `ic_env` cookie (used by `safeGetCanisterEnv()` from `@icp-sdk/core` to read canister IDs and the root key at runtime) is only served by asset canister Wasm versions >= `0.30.2`. The Wasm version is set via `configuration.version` in the recipe, independently of the recipe version itself. If you pin an older Wasm version, the cookie is silently missing and frontend code relying on `ic_env` will fail. Either omit `configuration.version` (latest is used) or pin to `0.30.2` or later. -8. **Not configuring `allow_raw_access` correctly.** The asset canister has two serving modes: certified (via `ic0.app` / `icp0.io`, where HTTP gateways verify response integrity) and raw (via `raw.ic0.app` / `raw.icp0.io`, where no verification occurs). By default, `allow_raw_access` is `true`, meaning assets are also available on the raw domain. On the raw domain, boundary nodes or a network-level attacker can tamper with response content undetected. Set `"allow_raw_access": false` in `.ic-assets.json5` for any sensitive assets. Only enable raw access when strictly needed. +8. **Not configuring `allow_raw_access` correctly.** The asset canister has two serving modes: certified (via `icp.net`, where HTTP gateways verify response integrity) and raw (via `raw.icp.net`, where no verification occurs). By default, `allow_raw_access` is `true`, meaning assets are also available on the raw domain. On the raw domain, boundary nodes or a network-level attacker can tamper with response content undetected. Set `"allow_raw_access": false` in `.ic-assets.json5` for any sensitive assets. Only enable raw access when strictly needed. 9. **Downgrading the asset canister WASM version.** Upgrading a canister to an older WASM version can fail with "Cannot parse header" panics if the stable memory format changed between versions. Prefer the `@dfinity/asset-canister` recipe over `type: pre-built` with a manually specified WASM URL — the recipe loads the latest asset canister version automatically if not explicitly specified in `configuration.version`. If you must pin a version, ensure it matches or exceeds the version currently deployed on-chain. If a downgrade is intentional, use reinstall mode (`icp deploy --mode reinstall`) instead of upgrade — this wipes stable memory and all uploaded assets. @@ -155,7 +155,7 @@ import { readFileSync, readdirSync } from "fs"; // For browser frontends, use rootKey from safeGetCanisterEnv() instead (see // the internet-identity skill or icp-cli/references/binding-generation.md). const LOCAL_REPLICA = "http://localhost:8000"; -const MAINNET = "https://ic0.app"; +const MAINNET = "https://icp-api.io"; const host = LOCAL_REPLICA; // Change to MAINNET for production async function manageAssets() { @@ -291,7 +291,7 @@ icp canister call frontend http_request '(record { # 5. Open in browser # Local: http://.localhost:8000 -# Mainnet: https://.ic0.app +# Mainnet: https://.icp.net # 6. Get canister ID icp canister id frontend diff --git a/skills/certified-variables/SKILL.md b/skills/certified-variables/SKILL.md index 14daf7c..fdbc29d 100644 --- a/skills/certified-variables/SKILL.md +++ b/skills/certified-variables/SKILL.md @@ -476,7 +476,7 @@ icp canister call backend get '("nonexistent")' # Console should NOT show "Certificate verification failed" errors # 6. For HTTP certification (custom HTTP canister): -curl -v https://CANISTER_ID.ic0.app/path +curl -v https://CANISTER_ID.icp.net/path # Expected: Response headers include IC-Certificate # HTTP gateway verifies the certificate before forwarding to client ``` diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md index 5e9350b..6b1c4ff 100644 --- a/skills/custom-domains/SKILL.md +++ b/skills/custom-domains/SKILL.md @@ -12,7 +12,7 @@ metadata: ## What This Is -By default, canisters are accessible at `.icp0.io`. The custom domains service lets you serve any canister under your own domain (e.g., `yourdomain.com`). You configure DNS, deploy a domain ownership file to your canister, and register via a REST API. The HTTP gateways then handle TLS certificate provisioning, renewal, and routing automatically. +By default, canisters are accessible at `.icp.net`. The custom domains service lets you serve any canister under your own domain (e.g., `yourdomain.com`). You configure DNS, deploy a domain ownership file to your canister, and register via a REST API. The HTTP gateways then handle TLS certificate provisioning, renewal, and routing automatically. Custom domains work at the boundary node level — they map a domain to any canister ID via DNS. This works with any canister that can serve `/.well-known/ic-domains` over HTTP, not just asset canisters. That includes asset canisters, Juno satellites, and custom canisters implementing `http_request`. @@ -40,7 +40,7 @@ Custom domains work at the boundary node level — they map a domain to any cani 7. **Not explicitly registering the domain.** DNS configuration alone is not enough. You must call `POST /custom-domains/v1/CUSTOM_DOMAIN` to start registration. It is not automatic. -8. **Not setting `host` in HttpAgent on custom domains.** When serving from a custom domain, the `HttpAgent` cannot automatically infer the IC API host like it can on `icp0.io`. You must set `host: "https://icp-api.io"` explicitly for mainnet. +8. **Not setting `host` in HttpAgent on custom domains.** When serving from a custom domain, the `HttpAgent` cannot automatically infer the IC API host like it can on `icp.net`. You must set `host: "https://icp-api.io"` explicitly for mainnet. 9. **Forgetting alternative origins for Internet Identity.** II principals depend on the origin domain. Switching from a canister URL to a custom domain changes principals. Configure `.well-known/ii-alternative-origins` to keep the same principals. See the `internet-identity` skill. @@ -78,14 +78,14 @@ www.example.com ### Step 3: Deploy -Deploy your canister so that `/.well-known/ic-domains` is accessible at `https://.icp0.io/.well-known/ic-domains`. +Deploy your canister so that `/.well-known/ic-domains` is accessible at `https://.icp.net/.well-known/ic-domains`. ### Step 4: Validate Check DNS records and canister configuration before registering: ```bash -curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN/validate" | jq +curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN/validate" | jq ``` Success response: @@ -116,7 +116,7 @@ If validation fails, common errors and fixes: ### Step 5: Register ```bash -curl -sL -X POST "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +curl -sL -X POST "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ``` Success response: @@ -137,7 +137,7 @@ Success response: Poll until `registration_status` is `registered`: ```bash -curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ``` Status values: `registering` → `registered` (success), or `failed` (check error message). @@ -152,13 +152,13 @@ To point an existing custom domain at a different canister: 2. Notify the service: ```bash -curl -sL -X PATCH "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +curl -sL -X PATCH "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ``` 3. Check status: ```bash -curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ``` ## Removing a Custom Domain @@ -167,13 +167,13 @@ curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq 2. Notify the service: ```bash -curl -sL -X DELETE "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +curl -sL -X DELETE "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ``` 3. Confirm deletion (should return 404): ```bash -curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ``` ## HttpAgent Configuration @@ -194,13 +194,13 @@ const agent = await HttpAgent.create({ host }); # 1. Deploy your canister with the ic-domains file served at /.well-known/ic-domains # 2. Validate DNS + canister config -curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com/validate" | jq +curl -sL -X GET "https://icp.net/custom-domains/v1/yourdomain.com/validate" | jq # 3. Register -curl -sL -X POST "https://icp0.io/custom-domains/v1/yourdomain.com" | jq +curl -sL -X POST "https://icp.net/custom-domains/v1/yourdomain.com" | jq # 4. Poll until registered -curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com" | jq +curl -sL -X GET "https://icp.net/custom-domains/v1/yourdomain.com" | jq ``` ## Verify It Works @@ -217,11 +217,11 @@ dig CNAME _acme-challenge.yourdomain.com # Expected: _acme-challenge.yourdomain.com. CNAME _acme-challenge.yourdomain.com.icp2.io. # 2. Verify ic-domains file is served by the canister -curl -sL "https://.icp0.io/.well-known/ic-domains" +curl -sL "https://.icp.net/.well-known/ic-domains" # Expected: your domain listed # 3. Verify registration status is "registered" -curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com" | jq '.data.registration_status' +curl -sL -X GET "https://icp.net/custom-domains/v1/yourdomain.com" | jq '.data.registration_status' # Expected: "registered" # 4. Verify the custom domain serves your canister diff --git a/skills/deploy-to-cloud-engine/SKILL.md b/skills/deploy-to-cloud-engine/SKILL.md index 4c4c0c2..892336d 100644 --- a/skills/deploy-to-cloud-engine/SKILL.md +++ b/skills/deploy-to-cloud-engine/SKILL.md @@ -160,7 +160,7 @@ icp deploy -e ic --subnet - The `icp deploy` output reports the deployed canister ids. - The canisters appear on the engine's **Applications** page in the console; each canister's detail view offers an "Open in browser" link. - If you set the metadata in Step 2, the canisters are grouped under your `__META_PROJECT` name with their `__META_NAME` labels, and the main canister shows an "Open" button — instead of bare principal rows. -- A frontend (asset) canister is served at `https://.icp0.io`. +- A frontend (asset) canister is served at `https://.icp.net`. Report the deployed canister ids (and the frontend URL, if any) back to the user. diff --git a/skills/internet-identity/SKILL.md b/skills/internet-identity/SKILL.md index 8641fe0..63510a3 100644 --- a/skills/internet-identity/SKILL.md +++ b/skills/internet-identity/SKILL.md @@ -311,7 +311,7 @@ canisters: # II backend principal (required). List your local II principal too if tests run against it. trusted_attribute_signers: "rdmx6-jaaaa-aaaaa-aaadq-cai" # Allowed frontend origins, comma-separated (required). - frontend_origins: "https://your-app.icp0.io" + frontend_origins: "https://your-app.icp.net" # Trusted SSO domains, comma-separated (optional; omit to reject all sso:* keys). trusted_sso_domains: "your-org.com" ``` @@ -335,7 +335,7 @@ use std::cell::RefCell; use std::collections::HashSet; const II_PRINCIPAL: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai"; -const TRUSTED_ORIGIN: &str = "https://your-app.icp0.io"; +const TRUSTED_ORIGIN: &str = "https://your-app.icp.net"; const FRESHNESS_NS: u64 = 300_000_000_000; // 5 minutes thread_local! { From 5e6c01d18624075d5c7b956274162486a5e096ec Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 18 Jun 2026 09:34:49 +0200 Subject: [PATCH 2/4] fix(custom-domains): correct HttpAgent host guidance (Raymond's review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pitfall and HttpAgent Configuration section claimed the agent "cannot auto-detect the IC API host on custom domains, so you must set host explicitly." That is inaccurate for recent agents. Per @icp-sdk/core determineHost(), an omitted host defaults to the local replica for localhost/127.0.0.1 origins and to https://icp-api.io for every other origin — including custom domains and icp.net. So on a custom domain you do not need to set host; it already resolves to the mainnet API boundary nodes. Reframe the pitfall around the real mistake: pointing host at the custom domain (or window.location.origin), which serves only the HTTP gateway, not /api/v2, so calls fail. Update the eval case to match. --- evaluations/custom-domains.json | 8 ++++---- skills/custom-domains/SKILL.md | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json index c6c6ea8..59bb3d6 100644 --- a/evaluations/custom-domains.json +++ b/evaluations/custom-domains.json @@ -53,11 +53,11 @@ }, { "name": "HttpAgent host configuration", - "prompt": "My IC frontend works fine on icp.net but API calls fail when I access it through my custom domain. What's wrong? Just the fix.", + "prompt": "My IC frontend's canister calls fail when it's served from my custom domain, though they work on icp.net. I set the HttpAgent host to my custom domain. What's wrong? Just the fix.", "expected_behaviors": [ - "Identifies that HttpAgent cannot auto-detect the IC API host on custom domains", - "Shows setting host to https://icp-api.io for mainnet", - "Shows HttpAgent.create({ host }) or equivalent configuration" + "Identifies that the custom domain serves only the HTTP gateway, not the /api/v2 API endpoint, so host must not point at it", + "Says to leave host unset (it defaults to https://icp-api.io off-localhost) or set it explicitly to https://icp-api.io", + "Does NOT recommend setting host to the custom domain or window.location.origin" ] }, { diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md index 6b1c4ff..77a5982 100644 --- a/skills/custom-domains/SKILL.md +++ b/skills/custom-domains/SKILL.md @@ -40,7 +40,7 @@ Custom domains work at the boundary node level — they map a domain to any cani 7. **Not explicitly registering the domain.** DNS configuration alone is not enough. You must call `POST /custom-domains/v1/CUSTOM_DOMAIN` to start registration. It is not automatic. -8. **Not setting `host` in HttpAgent on custom domains.** When serving from a custom domain, the `HttpAgent` cannot automatically infer the IC API host like it can on `icp.net`. You must set `host: "https://icp-api.io"` explicitly for mainnet. +8. **Pointing `HttpAgent`'s `host` at your custom domain.** You usually do not need to set `host` at all. When `host` is omitted, a recent `HttpAgent` (`@icp-sdk/core`) defaults to the local replica for `localhost`/`127.0.0.1` origins and to `https://icp-api.io` (the mainnet API boundary nodes) for every other origin — including custom domains and `icp.net`. The actual mistake is forcing `host` to your custom domain (or `window.location.origin`): that domain serves only the HTTP gateway, not `/api/v2`, so canister calls fail. Leave `host` unset, or set it explicitly to `https://icp-api.io` — never the gateway domain. 9. **Forgetting alternative origins for Internet Identity.** II principals depend on the origin domain. Switching from a canister URL to a custom domain changes principals. Configure `.well-known/ii-alternative-origins` to keep the same principals. See the `internet-identity` skill. @@ -178,14 +178,19 @@ curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ## HttpAgent Configuration -On custom domains, the agent cannot auto-detect the IC API host. Set it explicitly: +You usually do not need to set `host`. When `host` is omitted, a recent `HttpAgent` defaults to the local replica for `localhost`/`127.0.0.1` origins and to `https://icp-api.io` (the mainnet API boundary nodes) for every other origin — including custom domains and `icp.net`. Do **not** point `host` at your custom domain: it serves only the HTTP gateway, not `/api/v2`, so calls would fail. ```typescript import { HttpAgent } from "@icp-sdk/core/agent"; -const isProduction = process.env.NODE_ENV === "production"; -const host = isProduction ? "https://icp-api.io" : undefined; -const agent = await HttpAgent.create({ host }); +// host omitted → local replica on localhost origins, https://icp-api.io elsewhere +const agent = await HttpAgent.create(); +``` + +In Node.js there is no `window.location` to detect the origin, so an omitted `host` always resolves to `https://icp-api.io`. Set `host` to your local replica URL when talking to a local network; for mainnet you can leave it unset or set it explicitly to the API endpoint (never the gateway): + +```typescript +const agent = await HttpAgent.create({ host: "https://icp-api.io" }); ``` ## Deploy & Test From 164b95932f696050dd5aaeaa3c63324df5f0ea92 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 18 Jun 2026 09:42:02 +0200 Subject: [PATCH 3/4] docs(custom-domains): make HttpAgent host instructions crystal clear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite the HttpAgent Configuration section with an explicit resolution table (what an omitted host resolves to per runtime) and a per-scenario "what to do" list covering all cases: - mainnet browser frontend (icp.net / custom domain / legacy gateways): leave host unset → resolves to icp-api.io - mainnet Node.js: unset or icp-api.io - local dev in the browser on *.localhost: unset (auto-detects replica) - local dev in Node.js: MUST set host to the local replica URL — no window.location to detect localhost, so omitted host hits mainnet - non-mainnet/custom network: MUST set host to that network's API endpoint — the icp-api.io default points at mainnet Clarify that host is the API endpoint, never the gateway domain the frontend is served from. Tighten pitfall 8 to point at the section. Add an eval for the Node.js-local case (omitted host silently hits mainnet), the most counterintuitive of the must-set-host scenarios. --- evaluations/custom-domains.json | 9 +++++++++ skills/custom-domains/SKILL.md | 33 ++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json index 59bb3d6..4521ca4 100644 --- a/evaluations/custom-domains.json +++ b/evaluations/custom-domains.json @@ -60,6 +60,15 @@ "Does NOT recommend setting host to the custom domain or window.location.origin" ] }, + { + "name": "HttpAgent host in a Node.js local script", + "prompt": "I have a Node.js script that talks to my canister on a local replica at http://127.0.0.1:4943 using HttpAgent from @icp-sdk/core. I did not set host. It keeps hitting mainnet instead of my local replica. Why, and what's the fix? Keep it brief.", + "expected_behaviors": [ + "Explains that in Node.js there is no window.location, so an omitted host defaults to https://icp-api.io (mainnet)", + "Fix is to set host explicitly to the local replica URL, e.g. http://127.0.0.1:4943", + "Does NOT claim the agent auto-detects the local replica in Node.js" + ] + }, { "name": "Domain update flow", "prompt": "I want to point my existing custom domain to a different canister. What do I need to do? Just the steps.", diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md index 77a5982..47acadd 100644 --- a/skills/custom-domains/SKILL.md +++ b/skills/custom-domains/SKILL.md @@ -40,7 +40,7 @@ Custom domains work at the boundary node level — they map a domain to any cani 7. **Not explicitly registering the domain.** DNS configuration alone is not enough. You must call `POST /custom-domains/v1/CUSTOM_DOMAIN` to start registration. It is not automatic. -8. **Pointing `HttpAgent`'s `host` at your custom domain.** You usually do not need to set `host` at all. When `host` is omitted, a recent `HttpAgent` (`@icp-sdk/core`) defaults to the local replica for `localhost`/`127.0.0.1` origins and to `https://icp-api.io` (the mainnet API boundary nodes) for every other origin — including custom domains and `icp.net`. The actual mistake is forcing `host` to your custom domain (or `window.location.origin`): that domain serves only the HTTP gateway, not `/api/v2`, so canister calls fail. Leave `host` unset, or set it explicitly to `https://icp-api.io` — never the gateway domain. +8. **Setting `HttpAgent`'s `host` to your custom domain.** `host` is the **API endpoint**, not the domain your frontend is served from. Your custom domain (like `icp.net` or `ic0.app`) is the HTTP gateway — it does not serve `/api/v2`, so pointing `host` at it (or at `window.location.origin`) makes canister calls fail. For a mainnet frontend you usually do not need to set `host` at all: a recent `@icp-sdk/core` `HttpAgent` resolves an omitted `host` to `https://icp-api.io` (the mainnet API boundary nodes) on custom domains and `icp.net`. Leave it unset, or set it explicitly to `https://icp-api.io` — never the gateway domain. The exceptions where you *must* set `host` (local development in Node.js, and non-mainnet/custom networks) are in **HttpAgent Configuration** below. 9. **Forgetting alternative origins for Internet Identity.** II principals depend on the origin domain. Switching from a canister URL to a custom domain changes principals. Configure `.well-known/ii-alternative-origins` to keep the same principals. See the `internet-identity` skill. @@ -178,19 +178,38 @@ curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ## HttpAgent Configuration -You usually do not need to set `host`. When `host` is omitted, a recent `HttpAgent` defaults to the local replica for `localhost`/`127.0.0.1` origins and to `https://icp-api.io` (the mainnet API boundary nodes) for every other origin — including custom domains and `icp.net`. Do **not** point `host` at your custom domain: it serves only the HTTP gateway, not `/api/v2`, so calls would fail. +`HttpAgent`'s `host` is the **API endpoint** the agent sends `/api/v2` calls to — it is *not* the domain your frontend is served from. The two are different: your frontend is served by the HTTP **gateway** (`icp.net`, `ic0.app`, or your custom domain), while canister calls go to the **API boundary nodes** (`https://icp-api.io` on mainnet). A gateway domain does not serve `/api/v2`. + +### What an omitted `host` resolves to + +When you do not pass `host`, a recent `@icp-sdk/core` `HttpAgent` picks the endpoint automatically: + +| Where the code runs | Resolved API host | +|---|---| +| Browser on `*.localhost` / `*.127.0.0.1` | the local replica (same origin) | +| Browser on `*.ic0.app` / `*.icp0.io` | `https://ic0.app` / `https://icp0.io` (legacy gateways that also serve the API) | +| Browser on a custom domain, `*.icp.net`, or anything else | `https://icp-api.io` | +| Node.js (no `window.location`) | `https://icp-api.io` | + +### What to do + +- **Mainnet — browser frontend** (served from `icp.net`, a custom domain, or legacy `ic0.app`/`icp0.io`): **leave `host` unset.** It resolves to `https://icp-api.io` (or an equivalent API-serving gateway) automatically. Optionally set `host: "https://icp-api.io"` to be explicit. **Never** set `host` to your custom domain or `window.location.origin` — that is the gateway, not the API, and calls will fail. +- **Mainnet — Node.js** (scripts, SSR, backends): leave `host` unset or set `host: "https://icp-api.io"`. Both reach mainnet. +- **Local development — browser on `*.localhost`**: leave `host` unset; it auto-detects the local replica. +- **Local development — Node.js**: you **must** set `host` to your local replica URL (e.g. `http://127.0.0.1:4943`). With `host` omitted there is no `window.location` to detect localhost, so it defaults to `https://icp-api.io` (mainnet) — wrong target. +- **A non-mainnet / custom network** (self-hosted replica, testnet, private boundary nodes): you **must** set `host` to that network's API endpoint. The omitted-`host` default of `https://icp-api.io` points at mainnet, not your network. ```typescript import { HttpAgent } from "@icp-sdk/core/agent"; -// host omitted → local replica on localhost origins, https://icp-api.io elsewhere +// Mainnet (browser or Node.js): host omitted → resolves to https://icp-api.io const agent = await HttpAgent.create(); -``` -In Node.js there is no `window.location` to detect the origin, so an omitted `host` always resolves to `https://icp-api.io`. Set `host` to your local replica URL when talking to a local network; for mainnet you can leave it unset or set it explicitly to the API endpoint (never the gateway): +// Local development in Node.js: you MUST set host (no window to auto-detect) +const localAgent = await HttpAgent.create({ host: "http://127.0.0.1:4943" }); -```typescript -const agent = await HttpAgent.create({ host: "https://icp-api.io" }); +// Custom / non-mainnet network: point host at that network's API endpoint +const customNetworkAgent = await HttpAgent.create({ host: "https://my-network-api.example" }); ``` ## Deploy & Test From 3bd20f03b1583ac4ea2202a49e3d3b5675ca2799 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 18 Jun 2026 09:49:54 +0200 Subject: [PATCH 4/4] docs(custom-domains): scope HttpAgent host guidance to mainnet only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom-domains skill covers only the mainnet IC custom-domains service — you cannot have a custom domain on a local replica or a custom network. Drop the local-development and custom-network HttpAgent cases (and the resolution table) added in the previous commit; they were out of scope and diluted the skill. The section now states only what is relevant: a mainnet frontend served from a custom domain does not need host set (it resolves to icp-api.io), and must never point host at the gateway/custom domain. Remove the matching out-of-scope Node.js-local eval case. --- evaluations/custom-domains.json | 11 +---------- skills/custom-domains/SKILL.md | 32 ++++++-------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json index 4521ca4..c2497b9 100644 --- a/evaluations/custom-domains.json +++ b/evaluations/custom-domains.json @@ -56,19 +56,10 @@ "prompt": "My IC frontend's canister calls fail when it's served from my custom domain, though they work on icp.net. I set the HttpAgent host to my custom domain. What's wrong? Just the fix.", "expected_behaviors": [ "Identifies that the custom domain serves only the HTTP gateway, not the /api/v2 API endpoint, so host must not point at it", - "Says to leave host unset (it defaults to https://icp-api.io off-localhost) or set it explicitly to https://icp-api.io", + "Says to leave host unset (a recent agent defaults it to https://icp-api.io on a custom domain) or set it explicitly to https://icp-api.io", "Does NOT recommend setting host to the custom domain or window.location.origin" ] }, - { - "name": "HttpAgent host in a Node.js local script", - "prompt": "I have a Node.js script that talks to my canister on a local replica at http://127.0.0.1:4943 using HttpAgent from @icp-sdk/core. I did not set host. It keeps hitting mainnet instead of my local replica. Why, and what's the fix? Keep it brief.", - "expected_behaviors": [ - "Explains that in Node.js there is no window.location, so an omitted host defaults to https://icp-api.io (mainnet)", - "Fix is to set host explicitly to the local replica URL, e.g. http://127.0.0.1:4943", - "Does NOT claim the agent auto-detects the local replica in Node.js" - ] - }, { "name": "Domain update flow", "prompt": "I want to point my existing custom domain to a different canister. What do I need to do? Just the steps.", diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md index 47acadd..f5ff87f 100644 --- a/skills/custom-domains/SKILL.md +++ b/skills/custom-domains/SKILL.md @@ -40,7 +40,7 @@ Custom domains work at the boundary node level — they map a domain to any cani 7. **Not explicitly registering the domain.** DNS configuration alone is not enough. You must call `POST /custom-domains/v1/CUSTOM_DOMAIN` to start registration. It is not automatic. -8. **Setting `HttpAgent`'s `host` to your custom domain.** `host` is the **API endpoint**, not the domain your frontend is served from. Your custom domain (like `icp.net` or `ic0.app`) is the HTTP gateway — it does not serve `/api/v2`, so pointing `host` at it (or at `window.location.origin`) makes canister calls fail. For a mainnet frontend you usually do not need to set `host` at all: a recent `@icp-sdk/core` `HttpAgent` resolves an omitted `host` to `https://icp-api.io` (the mainnet API boundary nodes) on custom domains and `icp.net`. Leave it unset, or set it explicitly to `https://icp-api.io` — never the gateway domain. The exceptions where you *must* set `host` (local development in Node.js, and non-mainnet/custom networks) are in **HttpAgent Configuration** below. +8. **Setting `HttpAgent`'s `host` to your custom domain.** `host` is the **API endpoint** canister calls go to, not the domain your frontend is served from. Your custom domain is the HTTP gateway — it does not serve `/api/v2`, so pointing `host` at it (or at `window.location.origin`) makes calls fail. You do not need to set `host`: a recent `@icp-sdk/core` `HttpAgent` resolves an omitted `host` to `https://icp-api.io` (the mainnet API boundary nodes) on a custom domain. Leave it unset, or set it explicitly to `https://icp-api.io` — never the gateway domain. 9. **Forgetting alternative origins for Internet Identity.** II principals depend on the origin domain. Switching from a canister URL to a custom domain changes principals. Configure `.well-known/ii-alternative-origins` to keep the same principals. See the `internet-identity` skill. @@ -178,38 +178,18 @@ curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq ## HttpAgent Configuration -`HttpAgent`'s `host` is the **API endpoint** the agent sends `/api/v2` calls to — it is *not* the domain your frontend is served from. The two are different: your frontend is served by the HTTP **gateway** (`icp.net`, `ic0.app`, or your custom domain), while canister calls go to the **API boundary nodes** (`https://icp-api.io` on mainnet). A gateway domain does not serve `/api/v2`. +A frontend served from your custom domain still makes its canister calls through the mainnet **API boundary nodes** (`https://icp-api.io`), not through the domain it is served from. `HttpAgent`'s `host` is that API endpoint — *not* your frontend's origin. The custom domain is the HTTP gateway and does not serve `/api/v2`. -### What an omitted `host` resolves to - -When you do not pass `host`, a recent `@icp-sdk/core` `HttpAgent` picks the endpoint automatically: - -| Where the code runs | Resolved API host | -|---|---| -| Browser on `*.localhost` / `*.127.0.0.1` | the local replica (same origin) | -| Browser on `*.ic0.app` / `*.icp0.io` | `https://ic0.app` / `https://icp0.io` (legacy gateways that also serve the API) | -| Browser on a custom domain, `*.icp.net`, or anything else | `https://icp-api.io` | -| Node.js (no `window.location`) | `https://icp-api.io` | - -### What to do - -- **Mainnet — browser frontend** (served from `icp.net`, a custom domain, or legacy `ic0.app`/`icp0.io`): **leave `host` unset.** It resolves to `https://icp-api.io` (or an equivalent API-serving gateway) automatically. Optionally set `host: "https://icp-api.io"` to be explicit. **Never** set `host` to your custom domain or `window.location.origin` — that is the gateway, not the API, and calls will fail. -- **Mainnet — Node.js** (scripts, SSR, backends): leave `host` unset or set `host: "https://icp-api.io"`. Both reach mainnet. -- **Local development — browser on `*.localhost`**: leave `host` unset; it auto-detects the local replica. -- **Local development — Node.js**: you **must** set `host` to your local replica URL (e.g. `http://127.0.0.1:4943`). With `host` omitted there is no `window.location` to detect localhost, so it defaults to `https://icp-api.io` (mainnet) — wrong target. -- **A non-mainnet / custom network** (self-hosted replica, testnet, private boundary nodes): you **must** set `host` to that network's API endpoint. The omitted-`host` default of `https://icp-api.io` points at mainnet, not your network. +You do not need to set `host`. When it is omitted, a recent `@icp-sdk/core` `HttpAgent` resolves to `https://icp-api.io` on a custom domain (and on `icp.net`). Do **not** point `host` at your custom domain or `window.location.origin` — that is the gateway, so calls would fail. ```typescript import { HttpAgent } from "@icp-sdk/core/agent"; -// Mainnet (browser or Node.js): host omitted → resolves to https://icp-api.io +// host omitted on a custom domain → resolves to https://icp-api.io const agent = await HttpAgent.create(); -// Local development in Node.js: you MUST set host (no window to auto-detect) -const localAgent = await HttpAgent.create({ host: "http://127.0.0.1:4943" }); - -// Custom / non-mainnet network: point host at that network's API endpoint -const customNetworkAgent = await HttpAgent.create({ host: "https://my-network-api.example" }); +// equivalent, explicit: +const agentExplicit = await HttpAgent.create({ host: "https://icp-api.io" }); ``` ## Deploy & Test