> 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

Pull Oracle V2 lets your application stream Kaiko-signed price quotes, verify them on the Canton Network, pay a per-call fee in Canton Coin (or any pinned Token Standard instrument), and use the verified price inside your own Daml workflow. Verification, payment, and consumption all happen in a single atomic transaction.

This guide explains how the product works, how signatures and audit records are produced and checked, and how to onboard step by step, with request and response examples for every call.

**In this guide**

* [How Pull Oracle V2 works](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#how-pull-oracle-v2-works)
* [Signatures and the audit trail](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#signatures-and-the-audit-trail)
* [What VerifyAndPay guarantees](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#what-verifyandpay-guarantees)
* [Getting started](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#getting-started)
* [Auditing your calls](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#auditing-your-calls)
* [Operational guidance](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#operational-guidance)
* [Reference](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#reference)

### How Pull Oracle V2 works

#### The model

Pull Oracle V2 publishes nothing on-ledger ahead of consumption. No quote contract is written before you can read it, so there is no publish round-trip adding staleness for latency-sensitive workflows such as trade-at-oracle-price, liquidations, or settlement.

The flow has three parts:

1. Kaiko's off-ledger oracle backend, exposed through the Kaiko gRPC gateway, 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, oracle-signed verifier contract (`KaikoPaidQuoteVerifier`) lives on the ledger. It holds Kaiko's public key and pins the fee recipient (the `payee`). It is nonconsuming and global across all feeds, so one contract verifies any number of payloads concurrently.
3. Your application materializes the streamed payload in memory inside its own Daml choice and exercises `PaidQuoteVerifier_VerifyAndPay` on the disclosed verifier. The choice verifies the signature, settles the fee to the pinned payee via a direct Canton Token Standard transfer, writes a durable `AuditRecord`, and returns the verified quote. Your workflow consumes the quote in the same transaction.

At a glance, a single call looks like this:

1. Your application receives a payload, its signature, and the verifier disclosure from the Kaiko gRPC stream.
2. Your application selects Canton Coin holdings on its own participant and fetches a transfer factory and choice context from a registry Scan.
3. Your application submits one transaction that verifies the signature, pays the fee, creates the audit record, and uses the price.

If any step inside the transaction fails, the whole transaction rolls back. Nothing is paid and no partial state is left behind.

#### Components

<table><thead><tr><th width="179.87890625">Component</th><th width="207.7734375">Where</th><th width="457.7734375">Role</th></tr></thead><tbody><tr><td>Oracle backend</td><td>Kaiko-operated</td><td>Signs each payload off-ledger, serves <code>StreamCantonOracleServiceV2</code>, and attaches the verifier disclosure to every response.</td></tr><tr><td>Interface packages</td><td><code>canton-data-standard</code></td><td>The Canton Data Standard: <code>DataStandard.PaidQuoteVerifierV1</code> (the <code>PaidQuoteVerifier</code> interface and all verification and settlement logic), <code>DataStandard.PaidQuoteVerifierAuditV1</code> (the <code>AuditRecord</code> template), <code>DataStandard.Utils</code> (<code>Quote</code>, <code>VerificationAudit</code>), plus the vendored Canton Token Standard DARs (<code>splice-api-token-*-v1</code>).</td></tr><tr><td>Verifier package</td><td><code>kaiko-pull-oracle-v2</code></td><td><code>KaikoPaidQuoteVerifier</code>, Kaiko's concrete implementation of <code>PaidQuoteVerifier</code>. It supplies only the interface view and a key-rotation choice. All checks live in the standard.</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 you can use as-is or as a template.</td></tr></tbody></table>

#### Why this design

* **Verification, payment, and quote usage in one transaction.** A quote that does not verify is never paid for. A fee that does not settle never yields a quote. Your downstream action (trade, liquidation, settlement) commits with them or not at all. There is no partial state to reconcile.
* **No staleness from publishing.** The quote goes from Kaiko's signer to your choice without a synchronizer round-trip. The only on-ledger writes are the fee transfer and the audit record, both inside your own transaction.
* **One-step payment through the payee's standing pre-approval.** Kaiko's fee payee maintains a Canton Coin transfer pre-approval, so the fee settles as a `direct` Token Standard transfer inside the choice. There is no offer and accept round-trip, no payment channel to manage, and no prepaid balance.
* **Standard interfaces, no lock-in.** Your consumer code depends only on the Canton Data Standard's interface packages, never on a Kaiko template. Any standard-conforming paid verifier works with the same consumer.
* **A durable, self-contained audit trail.** Every successful verification writes an `AuditRecord` carrying everything needed to re-verify the quote off-ledger, years later, with no help from Kaiko.

#### What the Canton Data Standard provides

The `kaiko-pull-oracle-v2` DAR is deliberately thin. Everything that matters is inherited from `canton-data-standard`:

* `DataStandard.PaidQuoteVerifierV1` contains the `PaidQuoteVerifier` interface, its view (`oracle`, `payee`, `publicKey`, `hashMethod = "SHA-256"`, `signMethod = "secp256k1"`, `payloadCodec = "v1-paid-quote-concat"`), the argument types (`PaidSignedPayload`, `Cost`, `PaymentArgs`), the return type (`VerifiedPayload`), and the concrete choice body of `PaidQuoteVerifier_VerifyAndPay`. Implementations cannot override the checks.
* `DataStandard.PaidQuoteVerifierAuditV1` contains the `AuditRecord` template the choice creates on every success.
* `DataStandard.Utils` contains the shared `Quote` and `VerificationAudit` types.
* The pinned Canton Token Standard interface DARs (`splice-api-token-holding-v1`, `splice-api-token-transfer-instruction-v1`, `splice-api-token-metadata-v1`) are the interfaces through which the fee settles.

Your consumer's `daml.yaml` needs the Data Standard interface DARs and the Token Standard DARs on `data-dependencies`, and nothing Kaiko-specific.

### Signatures and the audit trail

#### Canonical encoding

The oracle does not sign JSON. It signs a deterministic concatenation of the payload's fields as text, called `v1-paid-quote-concat`. Every field is terminated by the reserved delimiter `|`, including the last one:

```
publishedAt | expiresAt | quote.feedId | quote.price | quote.priceTime | cost.fee | cost.instrument.admin | cost.instrument.id | payee |
```

The formatting rules are load-bearing. If your rendering is one byte off, verification fails.

<table><thead><tr><th>Field(s)</th><th width="520.87109375">Rendering</th></tr></thead><tbody><tr><td><code>publishedAt</code>, <code>expiresAt</code>, <code>priceTime</code></td><td>Integer milliseconds since the Unix epoch. Payloads never carry sub-millisecond precision.</td></tr><tr><td><code>price</code>, <code>fee</code></td><td>Daml <code>show</code> for <code>Decimal</code>: shortest exact form, at least one fractional digit, no trailing zeros, no exponent. Examples: <code>100</code> renders as <code>"100.0"</code>, <code>1.5</code> as <code>"1.5"</code>, <code>65000.12</code> as <code>"65000.12"</code>.</td></tr><tr><td><code>instrument.admin</code>, <code>payee</code></td><td>The fully qualified Canton party id (<code>partyToText</code>), that is <code>&#x3C;hint>::&#x3C;fingerprint></code>, never the bare hint.</td></tr><tr><td><code>feedId</code>, <code>instrument.id</code></td><td>Verbatim. They must not contain the reserved delimiter. The verifier rejects such payloads on-ledger before checking the signature, which makes the encoding injective.</td></tr></tbody></table>

Example canonical text (one line):

```
1784106761512|1784107361512|KK_BRR_ETHUSD|3421.87|1784106761000|1.5|DSO::1220a1b2c3…|Amulet|kaiko-payee::1220c3d4e5…|
```

You can reproduce the canonical text and its SHA-256 offline, with no ledger. It takes a few lines in any language: render each field with the rules above, concatenate with the trailing delimiters, and take the SHA-256 of the resulting bytes. In the example, `2026-07-15T09:12:41.512Z` renders as `1784106761512`.

#### How Kaiko signs each payload

For each streamed message, the backend:

1. Builds the canonical text above.
2. Computes the SHA-256 of the canonical text's bytes.
3. Signs that digest with ECDSA over secp256k1 using Kaiko's private key.
4. Encodes the signature as DER (`SEQUENCE(INTEGER r, INTEGER s)`) and hex-encodes it. That hex string is the `signature` field of the stream response.

The matching public key, hex-encoded DER (`SubjectPublicKeyInfo`), is resident on the `KaikoPaidQuoteVerifier` contract, not passed per call, so a caller can never substitute its own key.

#### How the ledger verifies

Inside `PaidQuoteVerifier_VerifyAndPay`, the Data Standard re-derives the canonical text from the `PaidSignedPayload` you pass in and checks:

```daml
secp256k1 signature (toHex (paidCanonicalText payload)) publicKey
```

The `secp256k1` builtin (from `DA.Crypto.Text`) applies SHA-256 to its message argument internally, so the on-ledger check and the off-ledger signer commit to exactly the same digest. If any field you submit differs by one byte from what Kaiko signed (price, fee, timestamps, payee), the canonical text differs, the digest differs, and the choice aborts with `invalid signature`.

#### The AuditRecord contract

On every successful verification, the choice body (defined in `canton-data-standard`, not in any Kaiko package) creates an `AuditRecord`, using the template `#canton-data-standard-paid-quote-verifier-audit-v1:DataStandard.PaidQuoteVerifierAuditV1:AuditRecord`. The record is signed by the oracle (`audit.distributor`, whose authority comes from its signature on the verifier contract), with the verifying subscriber (`audit.verifier`) as observer.

It carries every field the signature was computed over, plus provenance:

<table><thead><tr><th width="200.171875">Field</th><th>Content</th></tr></thead><tbody><tr><td><code>audit.verifier</code></td><td>The party that verified and paid: 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, never from the payload, so a record cannot claim a distributor the payload was not signed by.</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>Hex SHA-256 of the canonical text.</td></tr><tr><td><code>audit.signature</code></td><td>The oracle's hex DER signature, retained as evidence.</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>: the authenticated economic content.</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, equal to both the signed payee and the verifier's pinned payee. The choice enforced the match before settling.</td></tr></tbody></table>

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

#### Re-verifying a quote after the fact

An auditor holding an `AuditRecord` can re-verify the quote entirely off-ledger, with nothing beyond the record itself:

1. **Reconstruct** the canonical text from the record's fields, using the rules in [Canonical encoding](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#canonical-encoding): `audit.publishedAt | audit.expiresAt | quote.feedId | quote.price | quote.priceTime | fee | instrument.admin | instrument.id | payee |`, with times as epoch milliseconds and decimals as Daml `show`.
2. **Hash** it: take the SHA-256 of the canonical text's bytes.
3. **Verify** `audit.signature` (hex DER ECDSA, secp256k1) against `audit.publicKey` over that digest. A valid signature proves the oracle signed exactly this quote, this fee, and this payee.

Before (or instead of) the signature check, compare your step 2 digest with `audit.canonicalHash`. Equality confirms your reconstruction matches the payload that was actually verified on-ledger. It pins the record to one quote and no other, and localizes any mismatch to a re-encoding error rather than a forged signature.

### What VerifyAndPay guarantees

`PaidQuoteVerifier_VerifyAndPay` runs these checks in order, each with a deterministic abort message. Any failure rolls back the whole transaction: no payment, no data, no partial state.

<table><thead><tr><th width="87.80859375">Order</th><th>Check</th><th>Abort message</th></tr></thead><tbody><tr><td>1</td><td><code>publishedAt &#x3C;= expiresAt</code> (the window is well formed)</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><code>quote.feedId</code> carries no reserved delimiter</td><td><code>feedId contains the reserved delimiter</code></td></tr><tr><td>4</td><td><code>cost.instrument.id</code> carries no reserved delimiter</td><td><code>instrument id contains the reserved delimiter</code></td></tr><tr><td>5</td><td>The signature is valid over the canonical text, including cost and payee</td><td><code>invalid signature</code></td></tr><tr><td>6</td><td>The signed <code>payee</code> equals the verifier's pinned <code>payee</code></td><td><code>payee mismatch</code></td></tr><tr><td>7</td><td><code>cost.fee > 0.0</code></td><td><code>cost fee must be positive</code></td></tr><tr><td>8</td><td>The fee settles in one step (<code>TransferInstructionResult_Completed</code>)</td><td><code>fee did not settle in one step</code></td></tr><tr><td>9</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` and returns a `VerifiedPayload` (`distributor`, `publishedAt`, `quote`, `canonicalHash`, `signature`, `publicKey`) for your workflow to consume.

Three things remain your responsibility, on top of the standard:

* **Feed check.** The standard authenticates the signer, not the feed. Assert that `quote.feedId` is the feed you expect. The reference consumer aborts with `feed mismatch`.
* **Staleness policy.** The signed `expiresAt` is the standard's only replay defence. Bound `publishedAt` against your own maximum age if the default 10-minute window is too loose for your use case.
* **Payment inputs.** The amount, instrument, and recipient of the transfer are taken from the signed payload and the verifier's view, never from your `PaymentArgs`. The holdings and the registry context are yours to supply (see [Gather your payment inputs](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#gather-your-payment-inputs)).

The `payee mismatch` check protects the fee routing. Kaiko's public key is public, so anyone could stand up a look-alike verifier holding it but pinning a different payee. Binding the payee into the signature makes a genuine signature unusable through such a verifier.

### Getting started

Complete the steps in this section in order.

#### Prerequisites

* A Canton validator or participant node hosting your party and exposing the JSON Ledger API, plus an OAuth2 bearer token for it.
* Canton Coin holdings on that party sufficient to cover per-call fees.
* A Kaiko API key with the Canton oracle entitlement. This is the `Authorization: Bearer` token of the stream.
* The DARs, all distributed by Kaiko: `kaiko-pull-oracle-v2-0.1.0.dar`, the `canton-data-standard` interface DARs it depends on, the pinned `splice-api-token-*-v1` DARs, and, if you use the reference consumer, `kaiko-pull-oracle-v2-consumer-0.1.0.dar`.
* `grpcurl` (or any gRPC client) and `curl`.
* Your machine's IP address whitelisted by the Super Validator whose Scan you target (see [Gather your payment inputs](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#gather-your-payment-inputs)).

#### Upload the DARs

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

Upload each DAR to your participant through the JSON Ledger API.

**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.1.0.dar
```

**Response** (HTTP 200)

```json
{}
```

Repeat for the Data Standard and Token Standard dependency DARs and for your consumer DAR. Upload order does not matter. The participant resolves package dependencies at upload time, but every referenced package must be present before you submit commands.

#### Implement or reuse a consumer contract

Your workflow contract references the verifier through the interface, as a `ContractId PaidQuoteVerifier` from `DataStandard.PaidQuoteVerifierV1`, never through a Kaiko template. The minimal shape:

```daml
module MyApp.OracleClient where

import DataStandard.PaidQuoteVerifierV1

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

    nonconsuming choice UseQuote : ()
      with
        expectedFeedId : Text
        payload   : PaidSignedPayload
        signature : Text
        payment   : PaymentArgs
      controller subscriber
      do
        v <- exercise verifierCid PaidQuoteVerifier_VerifyAndPay with
          actor = subscriber, payload, signature, payment
        assertMsg "feed mismatch" (v.quote.feedId == expectedFeedId)
        -- v.quote.price is now authenticated and paid for:
        -- trade, settle, liquidate ... in this same transaction.
        pure ()
```

Alternatively, use the consumer package distributed by Kaiko (`kaiko-pull-oracle-v2-consumer`). Its `PaidQuoteConsumer` template does exactly the above and additionally records a `VerifiedQuote` contract that re-exposes the verified price through the standard `PublishedQuote` interface, so downstream readers consume it like any pushed quote. The rest of this guide uses `PaidQuoteConsumer`; substitute your own template freely.

The `daml.yaml` data-dependencies for a custom consumer:

```yaml
data-dependencies:
  - dependencies/canton-data-standard-utils-v1-0.1.1.dar
  - dependencies/canton-data-standard-quote-v1-0.1.1.dar
  - dependencies/canton-data-standard-quote-verifier-v1-0.1.1.dar
  - dependencies/canton-data-standard-paid-quote-verifier-v1-0.1.1.dar
  - dependencies/canton-data-standard-paid-quote-verifier-audit-v1-0.1.1.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
```

#### Create your consumer contract

The constructor needs the verifier's contract id. Kaiko communicates it during onboarding, and every stream message carries it in the verifier disclosure (see [Subscribe to the quote stream](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#subscribe-to-the-quote-stream)). The two 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 use it for every call. Verify it exists with an active-contracts query on your participant, filtered to the `PaidQuoteConsumer` template (same query shape as in [Select your holdings](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#select-your-holdings)).

#### Gather your payment inputs

The fee transfer happens inside the verify choice. You never build a Canton Coin transfer yourself. You supply three inputs as `PaymentArgs`, plus the disclosed contracts attached at submission time:

| `PaymentArgs` field  | What it is                                                                          | Where you get it                                                                                                                                                 |
| -------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputHoldingCids`   | The Canton Coin holdings funding the fee                                            | Your own participant (see [Select your holdings](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#select-your-holdings))                              |
| `transferFactoryCid` | The Canton Coin transfer factory                                                    | The registry Scan (see [Get the transfer factory and context](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#get-the-transfer-factory-and-context)) |
| `context`            | The registry context that settles the transfer and carries the payee's pre-approval | The registry Scan (see [Get the transfer factory and context](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#get-the-transfer-factory-and-context)) |

> The amount and receiver of the transfer-factory call come from the signed payload (`cost.fee`, `payee`). Fetch at least one stream message first to learn them. In production you run the stream continuously and refresh the payment context per call.

**IP whitelisting.** Every request to `{SCAN}/registry/…` requires the IP address of the requester's machine to be whitelisted by the Super Validator node it targets. Request whitelisting from the operator of each Scan you use before onboarding: an unlisted IP is rejected before reaching the endpoints below. If you read from several Scans for redundancy (see [Operational guidance](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#operational-guidance)), each Super Validator must whitelist you separately.

**Identify the Canton Coin instrument**

Canton Coin is `id = "Amulet"`, administered by the network's DSO party. Resolve the admin from the registry's metadata endpoint.

**Request**

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

**Response**

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

Use `{ "admin": "<DSO party>", "id": "Amulet" }` as the instrument throughout. You do not choose the fee or the instrument. Both travel signed inside each payload, and you read them from it.

**Select your holdings**

Your holdings are private data, read from your own participant (not from Scan) with an active-contracts query filtered to the Token Standard `Holding` interface.

**Request** (active-contracts query on your own participant)

```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": { "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`. Select fresh holdings for each call: they are archived by the transfer (change is returned to you automatically as a new holding), and reusing a spent contract id fails the submission.

**Get the transfer factory and context**

One call to the registry (Scan) returns the transfer factory, the choice context, and the disclosed contracts the transaction must attach. This is the Canton Network Token Standard `getTransferFactory` operation. Call it with the same receiver (the signed `payee`), amount (the signed `cost.fee`), and instrument the oracle will use. The registry uses the receiver to select the settlement path and to include the payee's pre-approval in the disclosures. If you request it for anything else, the transfer will not match what the choice constructs.

**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": "00faceb0…",
        "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`). Pass it through unchanged, with an empty `meta`.
* `choiceContext.disclosedContracts` is attached to the submission (see [Verify, pay, and use the quote](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#verify-pay-and-use-the-quote)), not placed inside `context`.

**Check `transferKind`.** It must be `direct` for one-step settlement. The registry returns `direct` only because Kaiko's payee maintains a standing transfer pre-approval. A value of `offer` means no current pre-approval exists and the oracle call would abort with `fee did not settle in one step`. Surface that instead of submitting.

**The context is short-lived.** It is tied to the network's current mining round. Fetch it close to the oracle call and never cache it across rounds.

#### Subscribe to the quote stream

Each streamed message carries the signed payload, the signature, and the verifier disclosure: everything Kaiko-side that the verify call needs.

**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` is `"testnet"` or `"mainnet"` (required). It selects the Canton network the payloads and verifier target.
* `feeds` takes 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 keep arriving 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": "3045022100d6bd65c9367393397d1e47fd4d84c733d76817e9a8c3d875a4597059c9f34bba02204b3c0f7d1b8313a97c0f9d1d3f59eb56e1788aaa5863a4d9118a462292adb196",
    "canonical_hash": "d2ac26438dd1d1a23b34c280cc94d4e9a67cbf52cb3b1509c7e96962b08a24e1",
    "verifier_disclosure": "\np087a50c…KaikoOracle.PullOracleV2:KaikoPaidQuoteVerifier…CgMyLjES…"
}
```

What to read from it:

* `payload` mirrors the Daml `PaidSignedPayload` literal field for field: camelCase field names, times as ISO-8601 UTC strings, decimals as strings. You can pass it nearly verbatim as the choice argument. Inspect `cost.fee` and `expiresAt` before submitting; skipping a message costs nothing.
* `signature` is the hex DER secp256k1 signature. Only `payload` is covered by it; the routing fields and `canonical_hash` are not.
* `canonical_hash` is an informative pre-check only. 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`, which the stream does not carry. Its contract id is also what the consumer constructor pins (see [Create your consumer contract](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#create-your-consumer-contract)).

#### Verify, pay, and use the quote

Exercise your consumer's choice, passing the streamed payload and signature plus the `PaymentArgs` you gathered, and attach both disclosure sets: the verifier disclosure from the stream and the registry's disclosed contracts. On success, in one transaction: the signature is verified, the fee settles to Kaiko's payee, the standard's choice body creates the `AuditRecord`, and your workflow consumes the verified price.

The full round trip:

1. Receive a payload, signature, and verifier disclosure from the stream.
2. Check `cost.fee` and `expiresAt`. Skip the message if you do not want to pay; skipping costs nothing.
3. Query your own `Holding` contracts and pick `inputHoldingCids`.
4. Call the registry's transfer-factory endpoint with the signed payee, fee, and instrument. Receive `factoryId`, `transferKind = direct`, the context, and the disclosed contracts.
5. Exercise your consumer choice, attaching the verifier disclosure and the registry's disclosed contracts.
6. The ledger commits one atomic transaction: signature check, payee check, fee transfer, `AuditRecord` creation, and consumption of the `VerifiedPayload` by your workflow. On any failure the transaction aborts deterministically and nothing is charged.

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

The streamed `payload` object is passed nearly verbatim as the choice argument. 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}`.

```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…" }
                }
              },
              "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…"
    },
    {
      "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": "00faceb0…",
      "createdEventBlob": "CgMyLjES…",
      "synchronizerId": "global-domain::1220e1e5…"
    }
  ]
}
```

**Response** (HTTP 200)

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

A failed call surfaces one of the deterministic abort messages listed in [What VerifyAndPay guarantees](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#what-verifyandpay-guarantees) (plus the reference consumer's own `feed mismatch` and `quote published in the future`) and charges nothing.

### Auditing your calls

Every successful `PaidQuoteVerifier_VerifyAndPay` leaves an `AuditRecord`, signed by the oracle and observed by you, so both sides hold the same durable evidence. List yours with an active-contracts query filtered to the `AuditRecord` template.

**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": "#canton-data-standard-paid-quote-verifier-audit-v1:DataStandard.PaidQuoteVerifierAuditV1: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 one record off-ledger, follow [Re-verifying a quote after the fact](https://claude.ai/chat/2f49723d-1bf1-484c-b493-7d89bf97b653#re-verifying-a-quote-after-the-fact): reconstruct the canonical text from its fields, compare its SHA-256 with `canonicalHash`, then check `signature` against `publicKey` with any secp256k1 and DER library. The reference consumer additionally records a `VerifiedQuote` per call, readable on-ledger through the standard `PublishedQuote` interface.

### Operational guidance

* **Refresh the registry context per call.** The transfer-factory context is tied to the current mining round. Fetch it just before submitting.
* **DevNet and TestNet resets.** Both are reset several times a year. The DSO party, the verifier contract id, and all contract ids change, so re-run the consumer creation and payment-input steps. MainNet is never reset.
* **Key rotation.** Kaiko rotates its signing key by creating a new verifier contract and switching the served disclosure, leaving the old one active until in-flight `expiresAt` horizons lapse. Always take the verifier disclosure from the current stream message rather than caching it long-term. After a rotation, recreate your consumer contract with the new verifier contract id.
* **Concurrency.** The verifier and the reference consumer are both nonconsuming, so any number of verify calls run concurrently. The only contention is your own holdings; allocate distinct `inputHoldingCids` to concurrent calls.
* **Multiple Scans.** Any Scan lists all Super Validator Scans via `GET {SCAN}/api/scan/v0/scans`. For production, read several and reconcile rather than depending on one operator.
* **Trust boundary.** The transfer factory and holdings are caller-supplied Token Standard implementations. The choice confirms a settlement was reported, not that value moved: the same package-vetting trust every Token Standard transfer carries.

### Reference

**Scan base URLs (Canton Foundation)**

<table><thead><tr><th width="106.66796875">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>`
* Entitlements: `nav` feeds require the Canton-oracle NAV entitlement; all other categories require the Canton-oracle entitlement.

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