Skip to content

feat!: price HTTPS outcalls with pay-as-you-go pricing (6.0.0) - #21

Merged
Kamirus merged 4 commits into
masterfrom
feat/http-outcall-pricing-v2
Nov 2, 2026
Merged

Kamirus merged 4 commits into
masterfrom
feat/http-outcall-pricing-v2

Conversation

@Kamirus

@Kamirus Kamirus commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Call.httpRequest now attaches a pricing version 2 ("pay-as-you-go") reservation instead of the version 1 cost. Version 1 charges for max_response_bytes, which is 2 MB when unset. Version 2 charges for the bytes and round-trip time a call actually uses and refunds the rest. Costs come from Prim.costHttpRequestV2, which moc 2.1.0 exposes.

Supersedes #16, rebuilt on the 5.0.0 types from #17. The builder design is from #16.

Before / After

// Before (5.0.0): version 1 pricing
let response = await Call.httpRequest(args);

// After: same arguments, version 2 pricing
let response = await Call.httpRequestFromArgs(args).send();

// Or build in place, and narrow the reservation
let response = await Call.httpRequest("https://example.com/api")
  .withMaxResponseBytes(4_000)
  .withExpectedRoundtripTimeMs(300)
  .send();

// New: flexible outcalls
let result = await Call.flexibleHttpRequest("https://example.com/api")
  .withReplication({ min_responses = 2; max_responses = 3; total_requests = 3 })
  .send();

The signature change is deliberate. Every call site fails to compile until it migrates, rather than switching pricing silently. Call.Cost.httpRequest(args, expected) now prices version 2. The version 1 Call.Cost.httpRequest(args) is gone, but Prim.costHttpRequest still exposes it.

[requirements] moc goes to 2.1.0. Merging publishes ic 6.0.0, so this is a draft.

Differences from #16

  • No Prim.subnetSelfNodeCount. moc imports every system API it references into each canister that imports mo:ic/Call, even a canister that only calls createCanister. Replicas without ic0.subnet_self_node_count, such as the ones in dfx 0.30.1 and 0.31.0, refuse to install those canisters. feat: pay-as-you-go pricing and flexible outcalls (5.0.0) #16 used the node count only to cap the default reservation when replication is unset. Without it, such a flexible outcall reserves for full-size responses, and the surplus is refunded. Setting replication restores the cap.
  • #flexible_ instead of #_351978059_. moc drops the trailing underscore when it maps to Candid, the same escape didc uses for #public_.
  • replication is passed through as given. When it's unset, the system applies its own defaults, the same as ic-cdk-management-canister.
  • Cost.httpRequestV2 is named Cost.httpRequest. It takes a second argument, so old call sites fail to compile.

Why CI changed

The dfx 0.30.1 replica that CI uses implements an earlier draft of ic0.cost_http_request_v2, with five parameters and no outcall_type. It rejects the specified encoding:

Canister violated contract: Failed to decode HttpRequestV2CostParams from Candid

PocketIC 16.1.0 implements the spec. However, under mops test PocketIC has no signing keys and makes no outcalls. The suite is now split:

  • test job: runs test/Call.test.mo on dfx 0.30.1 for create_canister, ECDSA, and Schnorr. The suite is unchanged except that the HTTP tests moved out.
  • test-pocket-ic job: runs test/HttpRequest.test.mo on PocketIC 16.1.0. It pins each builder's cost to ic0.cost_http_request_v2 on the parameters the spec defines, and checks that each setter moves the price the right way.

Both jobs use mops 2.24.0. mops publish would run every test file on a single replica, and neither replica passes both. So mops-publish.yml now runs the same two commands, then mops publish --no-test. As a side effect, mops.one won't show test results for 6.0.0.

No CI replica can both price version 2 and make an outcall, so nothing here sends a request end to end. send() is getCost() plus the management canister call.

🤖 Generated with Claude Code

Call.httpRequest becomes a builder that sets pricing_version = ?2 and
attaches the cost from Prim.costHttpRequestV2, which moc 2.1.0 exposes.
Call.flexibleHttpRequest is its counterpart for flexible_http_request.

The design follows #16. It drops Prim.subnetSelfNodeCount, because moc
imports it into every canister that imports Call, and replicas without
it refuse to install those canisters.

HTTPS outcall tests move to PocketIC 16.1.0. The dfx 0.30.1 replica
implements an earlier draft of ic0.cost_http_request_v2 that rejects
the specified parameters.

Co-authored-by: Leo Eichhorn <leo.eichhorn@dfinity.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread .github/workflows/mops-test.yml Fixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kamirus
Kamirus marked this pull request as ready for review November 2, 2026 12:02
@Kamirus
Kamirus requested a review from a team as a code owner November 2, 2026 12:02
Kamirus and others added 2 commits November 2, 2026 13:16
`mops publish` runs every test file on one replica, and no replica
passes both, so the publish workflow now runs the same split as CI and
publishes with `--no-test`.

Request bytes are UTF-8 lengths, as the system charges. `Text.size()`
counted characters and under-reserved for non-ASCII URLs and headers.

New tests pin each builder cost to `ic0.cost_http_request_v2` on the
parameters the spec defines, which catches swapped fields.

The CHANGELOG now states the replica requirement every `Call` importer
inherits, and no longer overstates the flexible per-node cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `test-pocket-ic` job now uses mops 2.24.0 with `--replica pocket-ic`,
the same combination the publish workflow runs, so a PR exercises it.

A golden test pins the transform path: context bytes, the encoded
function reference, and the 5 billion instruction maximum.

Docs no longer say HTTPS outcalls attach the minimum, describe the
per-node budget split, and say an outcall that exceeds its expectations
is rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kamirus
Kamirus merged commit fb50f52 into master Nov 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants