Skip to content

feat: pay-as-you-go pricing and flexible outcalls (5.0.0) - #16

Open
eichhorl wants to merge 1 commit into
caffeinelabs:masterfrom
eichhorl:eichhorl/pay-as-you-go-pricing
Open

eichhorl wants to merge 1 commit into
caffeinelabs:masterfrom
eichhorl:eichhorl/pay-as-you-go-pricing

Conversation

@eichhorl

@eichhorl eichhorl commented Sep 17, 2026 •

Copy link
Copy Markdown

Draft until a moc release carries the new primitives. [toolchain] moc is still 1.7.0, which lacks subnetSelfNodeCount and costHttpRequestV2, so mops test fails in CI. Blocked on caffeinelabs/motoko#6379.

Moves HTTPS outcalls to pricing version 2, which charges for the resources a call consumes rather than for max_response_bytes, and adds the flexible_http_request interface.

API

Call.httpRequest becomes a builder, and Call.flexibleHttpRequest is its counterpart for the new endpoint:

let response = await Call.httpRequest("https://example.com/api")
  .withMaxResponseBytes(4_000)
  .withExpectedRoundtripTimeMs(300)
  .withExpectedTransformInstructions(1_000_000)
  .send();

send() computes the cost with ic0.cost_http_request_v2 and attaches it. The four withExpected* setters narrow the reservation; anything left unset falls back to the most that parameter could reach, which is a reservation the call cannot exhaust but which holds far more cycles while it runs.

Cost.httpRequest (version 1) is removed and the builders always set pricing_version = 2.

See related cdk-rs PR here: dfinity/cdk-rs#714

Why this is a major bump

Two independent breaks, both deliberate.

HttpRequestArgs gains pricing_version : ?Nat32 from the upstream .did. Motoko requires every field in a record literal, so every existing hand-built HttpRequestArgs fails to compile. Callers using the builders are unaffected, since the builders set it.

Call.httpRequest changes from (IC.HttpRequestArgs) -> async IC.HttpRequestResult to (Text) -> HttpRequest. Switching the pricing version underneath an unchanged call would have been silent, which is what the Rust CDK avoided by deleting its free http_request. Call.httpRequestFromArgs(args) and Call.flexibleHttpRequestFromArgs(args) exist so a call site can migrate without rewriting how it builds its arguments; both overwrite pricing_version with 2, as documented on each.

Two things a reviewer should weigh

Shape. Eight wrappers in this package are Call.xxx(args) with a parallel Call.Cost.xxx(args); these two are builders, so they are the odd shape. The trade buys method-for-method parity with ic-cdk-management-canister 0.2, and puts the reservation setters on the happy path: with a single args-record wrapper there is nowhere to put an expectation, so narrowing would mean computing the cost and attaching cycles by hand. *FromArgs keeps the args-record door open.

The hashed variant label. The private cost parameters tag the flexible case as #_351978059_, the Candid field hash of "flexible", because flexible is a reserved word and the grammar admits only identifiers as variant labels. didc confirms it encodes byte-identically. It is confined to a private type; callers never see it. There is a comment at the definition explaining why.

Defaults carried over from the Rust CDK

  • A request with no transform reserves nothing for one, rather than reserving the full query instruction limit. On a 13-node subnet that term is otherwise the large majority of the reservation.
  • A flexible outcall's expected transformed size is capped at the block budget divided by min_responses, rounded up. Responses larger than that average cannot be delivered together, so reserving for them only withholds cycles.

Types

Regenerated from did/ic.did, which is byte-identical to public/references/ic.did on dfinity/developer-docs main as of the merge of dfinity/developer-docs#254. Adds flexible_http_request and its types, pricing_version, HttpRequestResourceReport, and picks up StatusVisibility, which had not been regenerated since it landed upstream.

Verification

moc --check on src/Call.mo, src/Types.mo and test/Call.test.mo is clean with zero warnings, using a locally built compiler carrying the two primitives. I also confirmed the dependency in both directions: it fails on the pinned moc 1.7.0 with both primitives reported missing, and passes on the built one.

Before merging

[requirements] moc and [toolchain] moc must both be raised to a release carrying the primitives. Merging a version bump to master publishes automatically, so this must not slip through.

Adds builders for both outcall methods and moves HTTPS outcalls to pricing
version 2, which charges for the resources a call consumes rather than for
max_response_bytes.

- Call.httpRequest / Call.flexibleHttpRequest return builders whose send()
  computes the cost with ic0.cost_http_request_v2 and attaches it. The four
  withExpected* setters narrow the reservation from the worst case to what the
  call is expected to use; anything left unset falls back to the maximum.
- Cost.httpRequest (version 1) is removed, and the builders always set
  pricing_version = 2. Migrating deliberately is the reason this is a major
  bump rather than a silent switch.
- Types regenerated from did/ic.did, which now carries flexible_http_request,
  pricing_version, and the per-node resource reports.

The flexible cost parameters tag the variant as #_351978059_, the Candid field
hash of "flexible", because flexible is a reserved word in Motoko and the
grammar admits only identifiers as variant labels. didc confirms it encodes
byte-identically. The tag is confined to a private type; callers never see it.

Requires a moc release carrying the subnetSelfNodeCount and costHttpRequestV2
primitives. [toolchain] moc is still 1.7.0, so CI fails until that lands, and
[requirements] moc must be raised before publishing.
@eichhorl eichhorl changed the title feat: add subnetSelfNodeCount and costHttpRequestV2 primitives feat: pay-as-you-go pricing and flexible outcalls (5.0.0) Sep 17, 2026
Kamirus added a commit that referenced this pull request Oct 9, 2026
The next version bump will fail to publish. `mops publish` runs the test
suite, and `mops-publish.yml` still installs the latest mops (3.x),
which dropped the dfx replica these tests need:

```
Tool 'pocket-ic' is not defined in [toolchain] section in mops.toml
Run mops toolchain use pocket-ic 15.0.0 to install it
```

[#18](#18) pinned mops
2.24.0 in `mops-test.yml` only, so PR checks pass while the publish job
is still exposed. That job hasn't run with mops 3 yet because no version
bump has landed since;
[#16](#16) and
[#17](#17) (both 5.0.0)
would be the first to hit it.

Moving these tests to mops 3 needs mops-side changes: configurable
test-canister cycles, auto-progress for HTTP outcalls, and a
`TestThresholdKeys` subnet. Out of scope here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant