> For the complete documentation index, see [llms.txt](https://docs.kaiko.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kaiko.com/on-chain/kaiko-data/kaiko-reference-rates/data-on-ramp/canton-pull-oracle.md).

# 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](#how-pull-oracle-v2-works)
* [Signatures and the audit trail](#signatures-and-the-audit-trail)
* [What VerifyAndPay guarantees](#what-verifyandpay-guarantees)
* [Getting started](#getting-started)
* [Auditing your calls](#auditing-your-calls)
* [Operational guidance](#operational-guidance)
* [Reference](#reference)

### 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](#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

<table><thead><tr><th width="172.9296875">Component</th><th>Where</th><th>Role</th></tr></thead><tbody><tr><td>Oracle backend</td><td>Kaiko-operated</td><td>Signs payloads off-ledger, serves <code>StreamCantonOracleServiceV2</code>, attaches the verifier disclosure to every response.</td></tr><tr><td>Standard packages</td><td><code>canton-data-standard</code> (KDS v0.2)</td><td><code>DataStandard.DistributorKeyV1</code> (<code>DistributorKey</code> interface publishing the signing key), <code>DataStandard.QuoteV1</code> (<code>PublishedQuote</code> interface), <code>DataStandard.Utils</code> (<code>Quote</code> and shared records), <code>canton-data-standard-codecs</code> (structural hash and signature-verification library).</td></tr><tr><td>Verifier package</td><td><code>kaiko-pull-oracle-v2</code></td><td><code>KaikoPaidQuoteVerifier</code> template. Implements <code>DistributorKey</code> and carries the <code>VerifyAndPay</code> choice (checks, fee settlement, audit-record creation). Also owns <code>KaikoOracle.PaidTypes</code> (<code>PaidSignedQuote</code>, <code>Cost</code>, <code>PaymentArgs</code>) and <code>KaikoOracle.PaidQuoteAudit</code> (<code>AuditRecord</code>, <code>VerificationAudit</code>).</td></tr><tr><td>Consumer package</td><td><code>kaiko-pull-oracle-v2-consumer</code></td><td><code>PaidQuoteConsumer</code> and <code>VerifiedQuote</code>, a reference subscriber workflow usable as-is or as a template.</td></tr><tr><td>Token Standard DARs</td><td>vendored by pull oracle V2</td><td>The pinned <code>splice-api-token-*-v1</code> DARs through which the fee settles.</td></tr></tbody></table>

#### 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:

```
hash(text)      = lowercase-hex SHA-256 of the text's UTF-8 bytes
hashRecord(hs)  = sha256("N|h1|...|hN")     -- N = count, hi = the field hashes
```

The root is derived in two layers:

```
hashQuote       = hashRecord [hash feedId, hash price, hash priceTime]
hashSignedQuote = hashRecord [hash publishedAt, hash expiresAt, hashQuote]
root            = hashRecord [hashSignedQuote, hash fee,
                              hash instrument.admin, hash instrument.id, hash payee]
```

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:

```daml
secp256k1 signature rootHashHex publicKey
```

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.

<table><thead><tr><th width="311.60546875">Field</th><th>Content</th></tr></thead><tbody><tr><td><code>audit.verifier</code></td><td>The choice's <code>actor</code>, meaning you.</td></tr><tr><td><code>audit.distributor</code></td><td>The oracle whose key validated the payload, copied from the verifier contract.</td></tr><tr><td><code>audit.verifiedAt</code></td><td>Ledger time of the verification.</td></tr><tr><td><code>audit.publishedAt</code>, <code>audit.expiresAt</code></td><td>The signed validity window.</td></tr><tr><td><code>audit.canonicalHash</code></td><td>The <code>v2-paid-quote-hash</code> root hash.</td></tr><tr><td><code>audit.signature</code></td><td>The oracle's hex DER signature.</td></tr><tr><td><code>audit.publicKey</code></td><td>The verifying key, copied from the verifier contract.</td></tr><tr><td><code>quote</code></td><td><code>feedId</code>, <code>price</code>, <code>priceTime</code>.</td></tr><tr><td><code>fee</code>, <code>instrument</code></td><td>The signed cost that was settled.</td></tr><tr><td><code>payee</code></td><td>The fee recipient.</td></tr></tbody></table>

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.

<table><thead><tr><th width="103.390625">Order</th><th width="344.0859375">Check</th><th>Abort message</th></tr></thead><tbody><tr><td>1</td><td><code>publishedAt &#x3C;= expiresAt</code></td><td><code>published after expiry</code></td></tr><tr><td>2</td><td><code>now &#x3C;= expiresAt</code></td><td><code>payload expired</code></td></tr><tr><td>3</td><td>Signature valid over the <code>v2-paid-quote-hash</code> root, including cost and payee</td><td><code>invalid signature</code></td></tr><tr><td>4</td><td>Signed <code>payee</code> equals the verifier's <code>payee</code></td><td><code>payee mismatch</code></td></tr><tr><td>5</td><td><code>cost.fee > 0.0</code></td><td><code>cost fee must be positive</code></td></tr><tr><td>6</td><td>Fee settles in one step (<code>TransferInstructionResult_Completed</code>)</td><td><code>fee did not settle in one step</code></td></tr><tr><td>7</td><td>Settlement credits at least one receiver holding</td><td><code>fee settlement produced no receiver holdings</code></td></tr></tbody></table>

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](#gather-your-payment-inputs)).

#### Upload the DARs

{% hint style="info" %}
Download the Pull Oracle DARs: <https://kaiko-delivery-links.s3.us-east-1.amazonaws.com/pull-oracle-v2.zip>
{% endhint %}

Request

```bash
curl -X POST {participant_node_url}/api/json-api/v2/packages \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @kaiko-pull-oracle-v2-0.2.0.dar
```

Response (HTTP 200)

```json
{}
```

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:

```daml
module MyApp.OracleClient where

import KaikoOracle.PullOracleV2
import KaikoOracle.PaidTypes

template MyOracleClient
  with
    subscriber : Party
    verifierCid : ContractId KaikoPaidQuoteVerifier   -- obtained by explicit disclosure
  where
    signatory subscriber

    nonconsuming choice UseQuote : ()
      with
        expectedFeedId : Text
        payload   : PaidSignedQuote
        signature : Text
        payment   : PaymentArgs
      controller subscriber
      do
        v <- exercise verifierCid VerifyAndPay with
          actor = subscriber, payload, signature, payment
          createAuditRecord = True
        assertMsg "feed mismatch" (v.quote.feedId == expectedFeedId)
        -- v.quote.price is now authenticated and paid for.
        pure ()
```

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:

```yaml
data-dependencies:
  - dependencies/canton-data-standard-utils-v1-0.2.0.dar
  - dependencies/canton-data-standard-quote-v1-0.2.0.dar
  - dependencies/canton-data-standard-distributor-key-v1-0.2.0.dar
  - dependencies/canton-data-standard-codecs-0.2.0.dar
  - dependencies/splice-api-token-metadata-v1-1.0.0.dar
  - dependencies/splice-api-token-holding-v1-1.0.0.dar
  - dependencies/splice-api-token-transfer-instruction-v1-1.0.0.dar
  - dependencies/kaiko-pull-oracle-v2-0.2.0.dar
```

#### 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`)

```json
{
  "commands": [
    {
      "CreateCommand": {
        "templateId": "#kaiko-pull-oracle-v2-consumer:KaikoOracle.PaidQuoteConsumer:PaidQuoteConsumer",
        "createArguments": {
          "subscriber": "myapp::1220f6a7b8…",
          "verifierCid": "00e5c1a4…"
        }
      }
    }
  ],
  "commandId": "create-consumer-1",
  "actAs": ["myapp::1220f6a7b8…"]
}
```

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

```bash
curl "{SCAN}/registry/metadata/v1/info"
```

Response

```json
{
  "adminId": "DSO::…",
  "supportedApis": { "splice-api-token-transfer-instruction-v1": 1 }
}
```

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

```bash
curl "{participant}/api/json-api/v2/state/active-contracts" \
  -H "Authorization: Bearer {token}" -H "Content-Type: application/json" \
  --data '{
    "activeAtOffset": <ledger-end>,
    "eventFormat": {
      "filtersByParty": {
        "MY_PARTY_ID": {
          "cumulative": [ { "identifierFilter": { "InterfaceFilter": {
            "value": { "interfaceId": "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding",
                       "includeInterfaceView": true } } } } ]
        }
      }
    }
  }'
```

Response (abridged, one entry per holding)

```json
[
  {
    "contractEntry": {
      "JsActiveContract": {
        "createdEvent": {
          "contractId": "00aa11…",
          "interfaceViews": [
            {
              "viewValue": {
                "owner": "myapp::1220f6a7b8…",
                "instrumentId": { "admin": "DSO::1220a1b2c3…", "id": "Amulet" },
                "amount": "25.0000000000",
                "lock": null
              }
            }
          ]
        }
      }
    }
  }
]
```

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`)

```bash
curl -X POST "{SCAN}/registry/transfer-instruction/v1/transfer-factory" \
  -H "Content-Type: application/json" \
  --data '{
    "choiceArguments": {
      "expectedAdmin": "DSO::1220a1b2c3…",
      "transfer": {
        "sender":   "myapp::1220f6a7b8…",
        "receiver": "kaiko-payee::1220c3d4e5…",
        "amount":   "1.5",
        "instrumentId": { "admin": "DSO::1220a1b2c3…", "id": "Amulet" },
        "requestedAt":   "2026-07-15T09:13:00.000000Z",
        "executeBefore": "2026-07-15T09:22:41.512000Z",
        "inputHoldingCids": ["00aa11…", "00bb22…"],
        "meta": { "values": {} }
      },
      "extraArgs": { "context": { "values": {} }, "meta": { "values": {} } }
    },
    "excludeDebugFields": true
  }'
```

Response (`TransferFactoryWithChoiceContext`, HTTP 200)

```json
{
  "factoryId": "00fac0de…",
  "transferKind": "direct",
  "choiceContext": {
    "choiceContextData": {
      "values": {
        "amulet-rules":                { "tag": "AV_ContractId", "value": "00c0ffee…" },
        "open-round":                  { "tag": "AV_ContractId", "value": "00d00d00…" },
        "transfer-preapproval":        { "tag": "AV_ContractId", "value": "00faceb0…" },
        "external-party-config-state": { "tag": "AV_ContractId", "value": "00a9f2fc…" }
      }
    },
    "disclosedContracts": [
      {
        "templateId": "…:Splice.AmuletRules:AmuletRules",
        "contractId": "00c0ffee…",
        "createdEventBlob": "CgMyLjES…",
        "synchronizerId": "global-domain::1220e1e5…"
      },
      {
        "templateId": "…:Splice.Round:OpenMiningRound",
        "contractId": "00d00d00…",
        "createdEventBlob": "CgMyLjES…",
        "synchronizerId": "global-domain::1220e1e5…"
      },
      {
        "templateId": "…:Splice.AmuletRules:TransferPreapproval",
        "contractId": "00faceb0…",
        "createdEventBlob": "CgMyLjES…",
        "synchronizerId": "global-domain::1220e1e5…"
      },
      {
        "templateId": "…:Splice.ExternalPartyConfigState:ExternalPartyConfigState",
        "contractId": "00a9f2fc…",
        "createdEventBlob": "CgMyLjES…",
        "synchronizerId": "global-domain::1220e1e5…"
      }
    ]
  }
}
```

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

```bash
grpcurl \
  -insecure \
  -H 'Authorization: Bearer {kaiko_api_key}' \
  -emit-defaults \
  -d '{"network":"testnet","feeds":[{"feed_category":"crypto","feed_id":"KK_BRR_ETHUSD"}]}' \
  'gateway-v0-grpc.kaiko.ovh:443' \
  kaikosdk.StreamCantonOracleServiceV2.Subscribe
```

* `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)

```json
{
    "network": "testnet",
    "feed_category": "crypto",
    "feed_id": "KK_BRR_ETHUSD",
    "payload": {
        "publishedAt": "2026-07-15T13:16:40.805000Z",
        "expiresAt": "2026-07-15T13:26:40.805000Z",
        "quote": {
            "feedId": "KK_BRR_ETHUSD",
            "price": "1935.86",
            "priceTime": "2026-07-15T13:16:40.794000Z"
        },
        "payee": "kaiko-payee::12208…",
        "cost": {
            "fee": "1.0",
            "instrument": {
                "admin": "DSO::1220f2…",
                "id": "Amulet"
            }
        }
    },
    "signature": "3045022100d6bd65c9…adb196",
    "canonical_hash": "d2ac26438dd1d1a23b34c280cc94d4e9a67cbf52cb3b1509c7e96962b08a24e1",
    "verifier_disclosure": "\np087a50c…KaikoOracle.PullOracleV2:KaikoPaidQuoteVerifier…CgMyLjES…"
}
```

* `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).

```json
{
  "commands": [
    {
      "ExerciseCommand": {
        "templateId": "#kaiko-pull-oracle-v2-consumer:KaikoOracle.PaidQuoteConsumer:PaidQuoteConsumer",
        "contractId": "004a9d02…",
        "choice": "VerifyAndRecordQuote",
        "choiceArgument": {
          "feedId": "KK_BRR_ETHUSD",
          "payload": {
            "publishedAt": "2026-07-15T09:12:41.512000Z",
            "expiresAt": "2026-07-15T09:22:41.512000Z",
            "cost": {
              "fee": "1.5",
              "instrument": { "admin": "DSO::1220a1b2c3…", "id": "Amulet" }
            },
            "payee": "kaiko-payee::1220c3d4e5…",
            "quote": {
              "feedId": "KK_BRR_ETHUSD",
              "price": "3421.87",
              "priceTime": "2026-07-15T09:12:41.000000Z"
            }
          },
          "signature": "3045022100e7c1…0220441a…",
          "payment": {
            "transferFactoryCid": "00fac0de…",
            "inputHoldingCids": ["00aa11…", "00bb22…"],
            "context": {
              "context": {
                "values": {
                  "amulet-rules":                { "tag": "AV_ContractId", "value": "00c0ffee…" },
                  "open-round":                  { "tag": "AV_ContractId", "value": "00d00d00…" },
                  "transfer-preapproval":        { "tag": "AV_ContractId", "value": "00faceb0…" },
                  "external-party-config-state": { "tag": "AV_ContractId", "value": "00a9f2fc…" }
                }
              },
              "meta": { "values": {} }
            }
          }
        }
      }
    }
  ],
  "commandId": "verify-1",
  "actAs": ["myapp::1220f6a7b8…"],
  "disclosedContracts": [
    {
      "templateId": "8be1ff…:KaikoOracle.PullOracleV2:KaikoPaidQuoteVerifier",
      "contractId": "00e5c1a4…",
      "createdEventBlob": "CgMyLjESlAYKRQ…",
      "synchronizerId": "global-domain::1220e1e5…"
    },
    {
      "templateId": "…:Splice.AmuletRules:AmuletRules",
      "contractId": "00c0ffee…",
      "createdEventBlob": "CgMyLjES…",
      "synchronizerId": "global-domain::1220e1e5…"
    }
  ]
}
```

Response (HTTP 200)

```json
{
  "updateId": "1220cafe…",
  "completionOffset": 4242
}
```

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](#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

```bash
curl "{participant}/api/json-api/v2/state/active-contracts" \
  -H "Authorization: Bearer {token}" -H "Content-Type: application/json" \
  --data '{
    "activeAtOffset": <ledger-end>,
    "eventFormat": {
      "filtersByParty": {
        "myapp::1220f6a7b8…": {
          "cumulative": [ { "identifierFilter": { "TemplateFilter": {
            "value": { "templateId": "#kaiko-pull-oracle-v2:KaikoOracle.PaidQuoteAudit:AuditRecord" } } } } ]
        }
      }
    }
  }'
```

Response (abridged, each entry's `createArguments`)

```json
{
  "audit": {
    "verifier": "myapp::1220f6a7b8…",
    "distributor": "kaiko-oracle::1220b2c3d4…",
    "verifiedAt": "2026-07-15T09:13:02.114Z",
    "publishedAt": "2026-07-15T09:12:41.512Z",
    "expiresAt": "2026-07-15T09:22:41.512Z",
    "canonicalHash": "9f0e2c47…",
    "signature": "3045022100e7c1…0220441a…",
    "publicKey": "3056301006072a8648ce3d020106052b8104000a034200…"
  },
  "quote": {
    "feedId": "KK_BRR_ETHUSD",
    "price": "3421.87",
    "priceTime": "2026-07-15T09:12:41Z"
  },
  "fee": "1.5",
  "instrument": { "admin": "DSO::1220a1b2c3…", "id": "Amulet" },
  "payee": "kaiko-payee::1220c3d4e5…"
}
```

To re-verify a record off-ledger, follow [Re-verifying a quote after the fact](#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)

<table><thead><tr><th width="126.83984375">Network</th><th>Scan base URL</th></tr></thead><tbody><tr><td>DevNet</td><td><code>https://scan.sv-1.dev.global.canton.network.sync.global</code></td></tr><tr><td>TestNet</td><td><code>https://scan.sv-1.test.global.canton.network.sync.global</code></td></tr><tr><td>MainNet</td><td><code>https://scan.sv-1.global.canton.network.sync.global</code></td></tr></tbody></table>

**Kaiko stream**

* Endpoint: `gateway-v0-grpc.kaiko.ovh:443`
* Method: `kaikosdk.StreamCantonOracleServiceV2.Subscribe`
* Authentication: `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>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kaiko.com/on-chain/kaiko-data/kaiko-reference-rates/data-on-ramp/canton-pull-oracle.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
