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:
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.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'sDistributorKeyinterface and exposes the verification flow as theVerifyAndPaytemplate choice.Your application passes the streamed payload into its own DAML choice and exercises
VerifyAndPayon the disclosed verifier. The choice verifies the signature, settles the fee to the payee through a direct Token Standard transfer, writes anAuditRecordwhen 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
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: theDistributorKeyinterface 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 byVerifyAndPay.DataStandard.Utils: theQuotetype and the payload and result records.DataStandard.QuoteV1: thePublishedQuoteinterface, implemented by the reference consumer'sVerifiedQuote.
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:
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
Build the
v2-paid-quote-hashroot hash.Decode the root hash's 64 hex characters to 32 bytes and take the SHA-256 of those bytes. That is the signed digest.
Sign the digest with ECDSA over secp256k1 using Kaiko's private key.
Encode the signature as DER (
SEQUENCE(INTEGER r, INTEGER s)) and hex-encode it. That string is thesignaturefield 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.
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:
Reconstruct the
v2-paid-quote-hashroot hash from the record's fields using the Canonical encoding rules: foldhashSignedQuotefromaudit.publishedAt,audit.expiresAt, andquote, then the root over it,fee,instrument.admin,instrument.id, andpayee.Compare your root with
audit.canonicalHash. A mismatch indicates a re-encoding error in step 1.Verify
audit.signature(hex DER ECDSA, secp256k1) againstaudit.publicKeyover 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.
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.feedIdis the feed you expect (the reference consumer aborts withfeed mismatch).Staleness policy.
expiresAtis the replay defense built into the choice. BoundpublishedAtagainst 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: Bearertoken of the stream).The DARs, distributed by Kaiko:
kaiko-pull-oracle-v2-0.2.0.dar, thecanton-data-standardv0.2 DARs (utils-v1,quote-v1,distributor-key-v1,codecs, all 0.2.0), the pinnedsplice-api-token-*-v1DARs and, if you use the reference consumer,kaiko-pull-oracle-v2-consumer-0.2.0.dar.grpcurl(or any gRPC client) andcurl.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:
factoryIdgoes toPaymentArgs.transferFactoryCid.choiceContext.choiceContextDatabecomes thecontextinsidePaymentArgs.context(anExtraArgs), passed through unchanged with an emptymeta.choiceContext.disclosedContractsgoes into the submission'sdisclosedContracts.
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 arecrypto,fx,equities,commodities,nav. Up to 100 feeds per stream.The method is server-streaming; messages arrive as prices tick.
Response (one streamed message)
payloadmirrors the DamlPaidSignedQuotefield for field (camelCase names, ISO-8601 UTC times, decimals as strings) and can be passed nearly verbatim as the choice argument. Checkcost.feeandexpiresAtbefore submitting.signatureis the hex DER secp256k1 signature overpayload, via the root hash.canonical_hashis thev2-paid-quote-hashroot, for pre-checking; the verifier recomputes it on-ledger.verifier_disclosureis the explicit-disclosure triple for the verifier whose key signed this payload. Pass it through unmodified as a disclosed contract, adding your network'ssynchronizerId. 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
expiresAthorizons lapse. Take the verifier disclosure from the current stream message.Concurrency: the verifier and reference consumer are nonconsuming. Allocate distinct
inputHoldingCidsto concurrent calls.
Reference
Scan base URLs (Canton Foundation)
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:443Method:
kaikosdk.StreamCantonOracleServiceV2.SubscribeAuthentication:
Authorization: Bearer <api key>
Documentation
Canton Network developer documentation: https://docs.canton.network/
Transfer-instruction API (transfer-factory endpoint): https://docs.canton.network/reference/splice-transfer-instruction-api/registrytransfer-instructionv1transfer-factory
Canton Network Token Standard (CIP-0056): https://github.com/global-synchronizer-foundation/cips/blob/main/cip-0056/cip-0056.md
Canton Coin specifics, including transfer pre-approvals: https://docs.digitalasset.com/integrate/devnet/canton-coin-specific-considerations/index.html
Discovering Super Validator Scan URLs: https://docs.sync.global/app_dev/scan_api/scan_global_synchronizer_connectivity_api.html
Last updated
Was this helpful?
