For the complete documentation index, see llms.txt. This page is also available as Markdown.

Canton pull oracle

A Canton pull oracle V2 streams Kaiko-signed price quotes to your application. Your application verifies a quote on the Canton Network, pays a per-call fee in Canton Coin (or any pinned Token Standard instrument), and uses the price in its own DAML workflow, in one atomic transaction.

This guide covers how the oracle works, how signatures and audit records are produced and checked, and how to onboard, with request and response examples.

In this guide

How pull oracle V2 works

Each call has three parts:

  1. Kaiko's off-ledger backend signs each quote payload with its secp256k1 key and streams it to you over kaikosdk.StreamCantonOracleServiceV2.Subscribe, together with the signature and an explicit-disclosure blob for the on-ledger verifier contract.

  2. A single long-lived verifier contract (KaikoPaidQuoteVerifier), signed by the oracle, holds Kaiko's public key and the fee recipient (payee). It is nonconsuming and shared across all feeds. It publishes its key through the Canton Data Standard's DistributorKey interface and exposes the verification flow as the VerifyAndPay template choice.

  3. Your application passes the streamed payload into its own DAML choice and exercises VerifyAndPay on the disclosed verifier. The choice verifies the signature, settles the fee to the payee through a direct Token Standard transfer, writes an AuditRecord when both audit gates are on (see The AuditRecord contract), and returns the verified quote for your workflow to consume in the same transaction.

If any step inside the transaction fails, the whole transaction rolls back, and no fee is charged.

Components

Component
Where
Role

Oracle backend

Kaiko-operated

Signs payloads off-ledger, serves StreamCantonOracleServiceV2, attaches the verifier disclosure to every response.

Standard packages

canton-data-standard (KDS v0.2)

DataStandard.DistributorKeyV1 (DistributorKey interface publishing the signing key), DataStandard.QuoteV1 (PublishedQuote interface), DataStandard.Utils (Quote and shared records), canton-data-standard-codecs (structural hash and signature-verification library).

Verifier package

kaiko-pull-oracle-v2

KaikoPaidQuoteVerifier template. Implements DistributorKey and carries the VerifyAndPay choice (checks, fee settlement, audit-record creation). Also owns KaikoOracle.PaidTypes (PaidSignedQuote, Cost, PaymentArgs) and KaikoOracle.PaidQuoteAudit (AuditRecord, VerificationAudit).

Consumer package

kaiko-pull-oracle-v2-consumer

PaidQuoteConsumer and VerifiedQuote, a reference subscriber workflow usable as-is or as a template.

Token Standard DARs

vendored by pull oracle V2

The pinned splice-api-token-*-v1 DARs through which the fee settles.

What the Canton Data Standard provides

  • DataStandard.DistributorKeyV1: the DistributorKey interface and its view (distributor, publicKey, signMethod = "secp256k1", hashMethod = "SHA-256", payloadCodec = "v2-paid-quote-hash").

  • canton-data-standard-codecs: the structural hash and signature checks called by VerifyAndPay.

  • DataStandard.Utils: the Quote type and the payload and result records.

  • DataStandard.QuoteV1: the PublishedQuote interface, implemented by the reference consumer's VerifiedQuote.

Your consumer's daml.yaml lists the Data Standard DARs, the Token Standard DARs, and the kaiko-pull-oracle-v2 DAR on data-dependencies (see Implement or reuse a consumer contract).

Signatures and the audit trail

Canonical encoding

The oracle signs a structural hash of the payload, v2-paid-quote-hash. Every field is hashed as text; records are hashed as a length-prefixed join of their field hashes:

The root is derived in two layers:

Scalar rendering must match exactly:

Field(s)
Rendering

publishedAt, expiresAt, priceTime

Integer milliseconds since the Unix epoch, as a decimal string. 2026-07-15T09:12:41.512Z renders as 1784106761512.

price, fee

Daml show for Decimal: shortest exact form, at least one fractional digit, no trailing zeros, no exponent. 100 renders as "100.0", 1.5 as "1.5", 65000.12 as "65000.12".

instrument.admin, payee

The fully qualified Canton party id (partyToText), <hint>::<fingerprint>.

feedId, instrument.id

Verbatim UTF-8

The root hash (64 lowercase hex characters) is the payload's canonicalHash. To reproduce it offline in any language: render each scalar, SHA-256 each one, fold the record joins. The golden-vector suite (the standard's codecs tests, mirrored in the Pull Oracle V2 repository by testGoldenVectors) is the normative reference.

How Kaiko signs each payload

  1. Build the v2-paid-quote-hash root hash.

  2. Decode the root hash's 64 hex characters to 32 bytes and take the SHA-256 of those bytes. That is the signed digest.

  3. Sign the digest with ECDSA over secp256k1 using Kaiko's private key.

  4. Encode the signature as DER (SEQUENCE(INTEGER r, INTEGER s)) and hex-encode it. That string is the signature field of the stream response.

The public key, hex-encoded DER (SubjectPublicKeyInfo), is stored on the KaikoPaidQuoteVerifier contract.

How the ledger verifies

Inside VerifyAndPay, the codecs library re-derives the root hash from the PaidSignedQuote you pass in and checks:

The secp256k1 builtin (DA.Crypto.Text) takes hex-encoded bytes and SHA-256s the decoded bytes internally, so on-ledger and off-ledger commit to the same digest. Any field that differs from what Kaiko signed changes the root hash, and the choice aborts with invalid signature.

The AuditRecord contract

On every successful verification with both audit gates on, the choice creates an AuditRecord (template #kaiko-pull-oracle-v2:KaikoOracle.PaidQuoteAudit:AuditRecord), signed by the oracle (audit.distributor) with the verifying subscriber (audit.verifier) as observer.

The two gates are the verifier's allowsAuditRecords template field (Kaiko's policy) and the choice's createAuditRecord flag (your per-call opt-in). A record is created when both are true.

Field
Content

audit.verifier

The choice's actor, meaning you.

audit.distributor

The oracle whose key validated the payload, copied from the verifier contract.

audit.verifiedAt

Ledger time of the verification.

audit.publishedAt, audit.expiresAt

The signed validity window.

audit.canonicalHash

The v2-paid-quote-hash root hash.

audit.signature

The oracle's hex DER signature.

audit.publicKey

The verifying key, copied from the verifier contract.

quote

feedId, price, priceTime.

fee, instrument

The signed cost that was settled.

payee

The fee recipient.

A non-stakeholder receiving the record by explicit disclosure reads it via the nonconsuming AuditRecord_Fetch choice.

Re-verifying a quote after the fact

From an AuditRecord alone, off-ledger:

  1. Reconstruct the v2-paid-quote-hash root hash from the record's fields using the Canonical encoding rules: fold hashSignedQuote from audit.publishedAt, audit.expiresAt, and quote, then the root over it, fee, instrument.admin, instrument.id, and payee.

  2. Compare your root with audit.canonicalHash. A mismatch indicates a re-encoding error in step 1.

  3. Verify audit.signature (hex DER ECDSA, secp256k1) against audit.publicKey over the SHA-256 of the root hash's decoded 32 bytes.

What VerifyAndPay guarantees

VerifyAndPay is a nonconsuming choice on KaikoPaidQuoteVerifier, taking actor, payload : PaidSignedQuote, signature, payment : PaymentArgs, and createAuditRecord. It runs these checks in order; any failure rolls back the whole transaction with a deterministic abort message and no fee is charged.

Order
Check
Abort message

1

publishedAt <= expiresAt

published after expiry

2

now <= expiresAt

payload expired

3

Signature valid over the v2-paid-quote-hash root, including cost and payee

invalid signature

4

Signed payee equals the verifier's payee

payee mismatch

5

cost.fee > 0.0

cost fee must be positive

6

Fee settles in one step (TransferInstructionResult_Completed)

fee did not settle in one step

7

Settlement credits at least one receiver holding

fee settlement produced no receiver holdings

On success, the choice creates the AuditRecord (when both gates are on) and returns the verified quote (distributor, publishedAt, quote, canonicalHash, signature, publicKey).

Your responsibilities on top of the choice:

  • Feed check. The choice authenticates the signer. Assert quote.feedId is the feed you expect (the reference consumer aborts with feed mismatch).

  • Staleness policy. expiresAt is the replay defense built into the choice. Bound publishedAt against your own maximum age if the default 10-minute window is too loose.

  • Payment inputs. Amount, instrument, and recipient come from the signed payload and the verifier contract. You supply the holdings and the registry context (see Gather your payment inputs).

Getting started

Complete the steps in order.

Prerequisites

  • A Canton validator or participant node hosting your party and exposing the JSON Ledger API, with an OAuth2 bearer token.

  • Canton Coin holdings on that party sufficient to cover per-call fees.

  • A Kaiko API key (the Authorization: Bearer token of the stream).

  • The DARs, distributed by Kaiko: kaiko-pull-oracle-v2-0.2.0.dar, the canton-data-standard v0.2 DARs (utils-v1, quote-v1, distributor-key-v1, codecs, all 0.2.0), the pinned splice-api-token-*-v1 DARs and, if you use the reference consumer, kaiko-pull-oracle-v2-consumer-0.2.0.dar.

  • grpcurl (or any gRPC client) and curl.

  • Your machine's IP whitelisted by the Super Validator whose Scan you target (see Gather your payment inputs).

Upload the DARs

Request

Response (HTTP 200)

Repeat for every dependency DAR and your consumer DAR, in any order, before submitting commands.

Implement or reuse a consumer contract

Your workflow contract references the verifier as a ContractId KaikoPaidQuoteVerifier. Minimal shape:

Alternatively, use Kaiko's kaiko-pull-oracle-v2-consumer package. Its PaidQuoteConsumer template does the above and records a VerifiedQuote contract exposing the price through the standard PublishedQuote interface. The rest of this guide uses PaidQuoteConsumer.

daml.yaml data-dependencies for a custom consumer:

Create your consumer contract

The constructor takes the verifier's contract id. Kaiko communicates it during onboarding; every stream message also carries it in the verifier disclosure. Both are the same value until a key rotation.

Request (POST {participant}/api/json-api/v2/commands/submit-and-wait)

The consumer is long-lived and nonconsuming: create it once and reuse it for every call.

Gather your payment inputs

The fee transfer runs inside the verify choice. You supply three inputs as PaymentArgs, plus disclosed contracts attached at submission:

PaymentArgs field

What it is

Where you get it

inputHoldingCids

Canton Coin holdings funding the fee

Your own participant

transferFactoryCid

The Canton Coin transfer factory

The registry Scan

context

The registry context that settles the transfer and carries the payee's pre-approval

The registry Scan

The amount and receiver come from the signed payload (cost.fee, payee), so fetch at least one stream message first. In production, refresh the payment context per call or use a cache.

Every request to {SCAN}/registry/… requires your IP to be whitelisted by the Super Validator operating that Scan.

Identify the Canton Coin instrument

Canton Coin is id = "Amulet", administered by the network's DSO party.

Request

Response

Use { "admin": "<DSO party>", "id": "Amulet" } as the instrument. Read the fee and instrument from each signed payload.

Select your holdings

Query your own participant for active contracts implementing the Token Standard Holding interface.

Request

Response (abridged, one entry per holding)

Pick unlocked holdings whose combined amount covers the fee; their contract ids form inputHoldingCids. Use fresh holdings for each call: the transfer archives the inputs and returns change as a new holding.

Get the transfer factory and context

One call to the registry returns the transfer factory, the choice context, and the disclosed contracts to attach. Use the signed payee as receiver, the signed cost.fee as amount, and the instrument from the payload; the registry uses the receiver to include the payee's pre-approval.

Request (requestedAt = now, executeBefore at or before the payload's expiresAt)

Response (TransferFactoryWithChoiceContext, HTTP 200)

Map the response into your call:

  • factoryId goes to PaymentArgs.transferFactoryCid.

  • choiceContext.choiceContextData becomes the context inside PaymentArgs.context (an ExtraArgs), passed through unchanged with an empty meta.

  • choiceContext.disclosedContracts goes into the submission's disclosedContracts.

Submit when transferKind is direct. A value of offer means the payee has no current pre-approval and the call would abort with fee did not settle in one step.

The context is tied to the current mining round. Fetch it just before the oracle call.

Subscribe to the quote stream

Request

  • network: "testnet" or "mainnet" (required).

  • feeds: at least one {feed_category, feed_id}; categories are crypto, fx, equities, commodities, nav. Up to 100 feeds per stream.

  • The method is server-streaming; messages arrive as prices tick.

Response (one streamed message)

  • payload mirrors the Daml PaidSignedQuote field for field (camelCase names, ISO-8601 UTC times, decimals as strings) and can be passed nearly verbatim as the choice argument. Check cost.fee and expiresAt before submitting.

  • signature is the hex DER secp256k1 signature over payload, via the root hash.

  • canonical_hash is the v2-paid-quote-hash root, for pre-checking; the verifier recomputes it on-ledger.

  • verifier_disclosure is the explicit-disclosure triple for the verifier whose key signed this payload. Pass it through unmodified as a disclosed contract, adding your network's synchronizerId. Its contract id is the one the consumer constructor pins.

Verify, pay, and use the quote

Exercise your consumer's choice with the streamed payload and signature plus your PaymentArgs, attaching both disclosure sets: the verifier disclosure from the stream and the registry's disclosed contracts.

Request (POST {participant}/api/json-api/v2/commands/submit-and-wait). The registry's choiceContextData is wrapped as ExtraArgs with an empty meta. disclosedContracts holds the verifier disclosure and every entry of the registry's disclosedContracts, each as {templateId, contractId, createdEventBlob, synchronizerId} (one registry entry shown below; attach all four).

Response (HTTP 200)

In one transaction: the signature is verified, the fee settles to the payee, the AuditRecord is created (when both audit gates are on), and your workflow consumes the verified price. A failed call returns one of the abort messages in What VerifyAndPay guarantees (or the reference consumer's feed mismatch / quote published in the future) and no fee is charged.

Auditing your calls

Every successful VerifyAndPay with the audit gates on leaves an AuditRecord visible to you. List yours:

Request

Response (abridged, each entry's createArguments)

To re-verify a record off-ledger, follow Re-verifying a quote after the fact. The reference consumer also records a VerifiedQuote per call, readable through the standard PublishedQuote interface.

Operational guidance

  • Refresh the registry context per call; it is tied to the current mining round.

  • DevNet and TestNet are reset several times a year. The DSO party, verifier contract id, and all contract ids change; re-run consumer creation and payment-input steps.

  • Key rotation: Kaiko creates a new verifier contract and switches the served disclosure, keeping the old one active until in-flight expiresAt horizons lapse. Take the verifier disclosure from the current stream message.

  • Concurrency: the verifier and reference consumer are nonconsuming. Allocate distinct inputHoldingCids to concurrent calls.

Reference

Scan base URLs (Canton Foundation)

Network
Scan base URL

DevNet

https://scan.sv-1.dev.global.canton.network.sync.global

TestNet

https://scan.sv-1.test.global.canton.network.sync.global

MainNet

https://scan.sv-1.global.canton.network.sync.global

Kaiko stream

  • Endpoint: gateway-v0-grpc.kaiko.ovh:443

  • Method: kaikosdk.StreamCantonOracleServiceV2.Subscribe

  • Authentication: Authorization: Bearer <api key>

Documentation

Last updated

Was this helpful?