CIP-0103: dApp Standard
CIP-0103: dApp Standard
Abstract
This proposal introduces a vendor-neutral dApp API that decouples network connectivity and key management from decentralized applications, while providing a standardized set of primitives for interacting with the Canton Network.
Using the dApp API, users can access any application with a validator and signing provider of their choice. In this model, dApp clients operate in conjunction with Wallets, which implement the dApp API, are trusted by the user, and enforce authorization to guard against malicious requests from untrusted dApps.
The design emphasizes a minimal, interoperable surface with strict, unambiguous semantics for requests, responses, and errors, while remaining flexible enough to support diverse Wallet architectures and deployment models.
As a result, any dApp client can seamlessly interoperate with any Wallet.
Copyright
This CIP is licensed under CC0-1.0: Creative Commons CC0 1.0 Universal
Motivation
Today, dApp clients built for the Canton Network are tightly coupled to specific network, key-management, and wallet integrations. The lack of a unified standard forces developers to repeatedly reimplement similar patterns, limits interoperability, and constrains user choice. Users cannot easily select the validator or signing provider they trust, and app developers must maintain multiple ad-hoc integration paths. This fragmentation introduces unnecessary friction, increases integration effort, and elevates security risks.
Existing interfaces, such as the gRPC and JSON Ledger APIs, provide low-level protocol access but do not address higher-level concerns: standard signing flows, user consent, cross-origin communication, or consistent error semantics. Without a formal standard, wallets and applications cannot reliably interoperate, preventing a healthy ecosystem and hindering modular innovation.
To address these challenges, the dApp API introduces a vendor-neutral, transport-agnostic framework for Canton-native dApps. By defining a shared interface for ledger interaction, transaction submission, and message signing, the API decouples dApp clients from wallet implementations while preserving security guarantees. Users can connect through any validator and sign using the method of their choice, while wallets enforce authorization to prevent misbinding and phishing from untrusted dApps. The API is also designed to accommodate future wallets, custody solutions, and deployment models, ensuring that new implementations can interoperate seamlessly with existing dApps and infrastructure.
The API is designed to decouple the following three kinds of services:
- Wallet and custody solution providers Responsible for identity, authorization, and signing. They implement the dApp API to enable secure user interaction with dApps.
- User-facing dApps Expose on-ledger actions (transfers, settlements, governance, etc.) through web or mobile interfaces.
- Application backends Programmatically create, query, and manage contracts and transactions on the Canton ledger.
The design emphasizes the following characteristics:
- Transport-agnostic: Any dApp (client) can interact with any wallet (server), independent of implementation or transport.
- Signing-provider agnostic: Supports internal and external signing, regardless of the private key's location.
- Multi-network support: Users may connect to any validator node and synchronizer supported by their wallet
- Minimal operation set: Standardizes primitives for ledger reads, ledger writes, and message signing.
- Standardized error model: Standardized error codes ensure predictable control flow and consistent telemetry.
- Secure: Private keys remain within the signing provider; user authorization is enforced, and the attack surface is minimized.
This CIP draws inspiration from widely adopted standards such as EIP-1193, while addressing Canton-specific requirements: privacy-preserving participants, multi-party workflows, and synchronized settlement. The dApp API can also be wrapped in a provider object, similar to Ethereum, allowing dApps to interact with a Wallet through a familiar interface. By providing a portable, secure, and interoperable foundation, the dApp API enables any dApp client to reliably interact with any wallet, reduces developer friction, improves user security, and supports a robust, modular Canton ecosystem.
Specification
Definitions
This section defines the terms used normatively throughout the specification. Each term is scoped to this CIP and, unless stated otherwise, applies to all methods, transports, and UX requirements. The goal is to ensure unambiguous meaning so that independent wallets and dApps interpret the interface consistently.
| Term | Definition | Notes |
|---|---|---|
| Client | Invokes RPC methods defined by the dApp API | Client adheres to the JSON-RPC specification, but defines the transport method |
| dApp | Decentralized Application.<br><br>The client side (typically a web app) consumes the dApp API. | Application with at least part of the backend and database hosted on a decentralized infrastructure, e.g., as a smart contract on Canton, combined with a client such as a UI frontend.<br><br>Given the nature of the topic, this document treats the dApp as primarily the UI. |
| Network | Network access parameters | Uniquely defined by: (Validator, Synchronizer, Authentication details) |
| Wallet | Provides the dApp API. Middleware between the client and the network, as well as the signing provider. | Implementation details are out of scope; MUST honor methods/events, UX, and error codes |
Provider API
The Provider API defines a set of methods that enable the uniform usage of the dApp API (see below). The methods in this interface are an exact replica of the Provider API defined in EIP-1193.
Below is the interface description in Typescript. Interface definitions in other languages that replicate the methods are equally valid:
type EventListener<T> = (...args: T[]) => void
type RequestParams = unknown[] | Record<string, unknown>
interface RequestPayload {
method: string
params?: RequestParams
}
interface Provider {
request<T>(args: RequestPayload): Promise<T>
on<T>(event: string, listener: EventListener<T>): Provider
emit<T>(event: string, ...args: T[]): boolean
removeListener<T>(
event: string,
listenerToRemove: EventListener<T>
): Provider
}
Using the interface above, dApp API requests are issued by specifying the appropriate method and corresponding params as defined in this specification. Similarly, dApp API events can be consumed by registering a listener for the corresponding event.
Error Types
It is the Provider’s responsibility to enforce a consistent error format. As noted above, this CIP adopts the conventions defined in EIP-1193, with error structure and codes further aligned with EIP-1474:
interface ProviderRpcError extends Error {
message: string
code: number
data?: unknown
}
Error member included on the response object MUST be an object containing a code member and a descriptive message member.
In addition to defining the error format, this CIP incorporates the error codes specified in EIP-1474. To ensure consistency with established ecosystem conventions, the following table enumerates all supported error codes and their corresponding messages:
| Code | Message | Meaning | EIP |
|---|---|---|---|
| 4001 | User Rejected Request | The user rejected the request. | EIP-1193 |
| 4100 | Unauthorized | The requested method and/or account has not been authorized by the user. | EIP-1193 |
| 4200 | Unsupported Method | The Provider does not support the requested method. | EIP-1193 |
| 4900 | Disconnected | The Provider is disconnected from all chains. | EIP-1193 |
| 4901 | Chain Disconnected | The Provider is not connected to the requested chain. | EIP-1193 |
| -32700 | Parse error | Invalid JSON | EIP-1474 |
| -32600 | Invalid request | JSON is not a valid request object | EIP-1474 |
| -32601 | Method not found | Method does not exist | EIP-1474 |
| -32602 | Invalid params | Invalid method parameters | EIP-1474 |
| -32603 | Internal error | Internal JSON-RPC error | EIP-1474 |
| -32000 | Invalid input | Missing or invalid parameters | EIP-1474 |
| -32001 | Resource not found | Requested resource not found | EIP-1474 |
| -32002 | Resource unavailable | Requested resource not available | EIP-1474 |
| -32003 | Transaction rejected | Transaction creation failed | EIP-1474 |
| -32004 | Method not supported | Method is not implemented | EIP-1474 |
| -32005 | Limit exceeded | Request exceeds defined limit | EIP-1474 |
Provider Discovery
A dApp MAY allow users to select from multiple available providers. While multiple providers may advertise their availability, the mechanism for such announcements is out of scope for this CIP, which focuses on the dApp API exposed by a single provider. We expect the mechanism for dealing with multiple providers to be specified in a future CIP.
dApp API
The dApp API defines a standardized, transport-agnostic interface that enables decentralized applications (dApps) to securely interact with the Canton Network via Wallets. It decouples dApps from wallet implementations, supports multiple network configurations, and enforces consistent signing flows, reducing developer friction and improving user security. By providing uniform primitives for ledger access, transaction submission, and message signing, the API ensures interoperability between independently developed dApps and Wallets.
The ground truth for the dApp API is maintained in the Splice Wallet repository in a machine-readable form. The methods and events listed in this document represent those that MUST be implemented by Wallet Providers and provide a convenient summary of the state of the API at the time of writing for readers of the CIP.
The dApp API exists in two variants, each targeting a different deployment model and interaction pattern:
-
Synchronous dApp API (aka Sync dApp API) – Intended for clients with direct access to a Wallet, such as browser extensions or local Wallets. Methods and events execute in a request–response fashion, enabling immediate ledger reads, transaction preparation, and signing flows.
-
Asynchronous dApp API (aka Async dApp API) – Designed for server-side Wallets or Wallets deployed as services that cannot support blocking calls or direct synchronous interactions. This variant decomposes operations requiring user interaction into multi-phase workflows, often exposing
userUrl’s to allow users to complete actions asynchronously (for example, by interacting with a UI) while the client process continues to wait for an event to be emitted (as a result of the user interaction).
Clients such as frontends that work against the sync dApp API SHOULD also work against a server exposing the async dApp API. This ensures that applications can support both local and remote Wallet configurations without changing core business logic. We expect frontends to do so by implementing the sync dApp API by relaying calls to an async dApp API server. See the reference dApp SDK implementation for a drop-in solution.
Synchronous dApp API
The Synchronous dApp API variant is intended for clients that have direct access to a Wallet, such as browser extensions, desktop applications, or mobile Wallets. In this model, methods and events are executed in a standard request–response fashion, allowing dApps to perform ledger reads, transaction preparation, and signing operations immediately and deterministically.
Wallet Providers implementing this variant MUST support all listed methods and events, ensuring consistent behavior and error semantics across deployments. This variant is the canonical choice for end-user applications, providing a familiar synchronous programming model and minimizing latency between user actions and dApp responses.
Overview of supported methods:
| Method | Output | Notes |
|---|---|---|
| connect | ConnectResult | Establishes a connection to the server (e.g. Wallet) |
| disconnect | void | Closes the session between the client and the provider |
| isConnected | ConnectResult | Indicates connectivity to the server (e.g. Wallet) |
| status | StatusEvent | Contains information regarding the connected Wallet and Network |
| getActiveNetwork | Network | Details of the connected network |
| listAccounts | Account[] | Lists all Accounts |
| getPrimaryAccount | Account | Single Account that is subject to the dApp |
| signMessage | string | Signs an arbitrary string |
| prepareExecute | Void | Prepares, signs, and executes the commands |
| ledgerApi | object | Proxies Ledger API |
Overview of supported events:
| Event | Paylaod | Notes |
|---|---|---|
| accountsChanged | AccountsChangedEvent | Contains all the accounts known to the provider and states which one is currently the primary account. |
| statusChanged | StatusEvent | Informs about the status of the provider the client is connected to; for example, whether or not the user is authenticated and/or connected to a network. |
| txChanged | TxChangedEvent | Announces changes to the lifecycle of initiated transactions. |
The following sections provide details on each endpoint.
connect
Returns a ConnectResult with isConnected = true if authentication between the user client and the dApp API server has been successfully established; otherwise, returns a ConnectResult with isConnected = false.
Additionally, the response MAY include an isNetworkConnected field (optionally accompanied by an error reason) indicating whether the dApp API server is currently connected to a Canton network.
- Network connection is considered to be established if an unauthenticated ledger endpoint, such as
/v2/version, can be accessed
If the user client does not have an active session with the server, the user login flow MUST be executed before a response is returned.
// ConnectResult
{
isConnected: boolean,
reason?: string
isNetworkConnected?: boolean,
networkReason?: string
}
isConnected
Same as connect, but does not initialize the login.
disconnect
Closes the connection between the user client and the dApp API server. As a result, the user session, involving the access token, is being invalidated on the server. In addition, the statusChanged event MAY be emitted as an asynchronous confirmation sent back to the dApp.
status
Returns the connectivity status as per connect and additional information regarding the connection and the dApp API server properties. The property kernel holds a unique identifier of the server (e.g., the Wallet component) and indicates the deployment type in the clientType field. In addition, the optional fields network and session MAY carry information about the user's session and the network to which the connection is established. However, if returned, the networkId SHOULD be CAIP-2 compliant, e.g., canton:da-mainnet.
// StatusEvent
{
connection: ConnectResult,
provider: Provider,
network?: Network,
session?: Session
}
// Provider
{
id: string,
version: string, // dApp API version
providerType: Enum[browser,desktop,mobile,remote]
}
// Network
{
networkId: string, // CAIP-2
ledgerApi?: string
accessToken?: string,
}
// Session
{
accessToken: string,
userId: string
}
getActiveNetwork
Returns the network (same type as seen in status) with which the user has established a connection via the dApp server.
listAccounts
Return a list of accounts the user has access to. Specifically, an account is a Canton party with additional metadata, as well as information about its status and the network where its private data can be accessed. In addition, the user chooses to use a given account as the primary account (and the corresponding network) in the context of the current dApp session.
// Account
{
primary: boolean,
partyId: string,
status: Enum[initializing, allocated],
hint: string,
publicKey: string,
namespace: string,
networkId: string,
signingProviderId: string
}
getPrimaryAccount
Returns the single account set to primary, provided the user has access to at least one account. See listAccounts above for type information.
Note: the Wallet Provider MUST define (or give the user the controls to define) exactly one party as the primary party, provided
signMessage
Takes an arbitrary string and returns the signature of the message received. The Wallet Provider MUST use the private key corresponding to the account with the primary property set to true, or, alternatively, give the user the option to sign using another account’s private key. Naturally, the public key of the same party can be used to verify the signature.
prepareExecute
Provides an abstraction over the prepare, sign, and execute steps required to submit a Daml transaction to a Canton participant node. Thereby, the Wallet Provider SHOULD support internal and external parties.
The request argument is compatible with the JsPrepareSubmissionRequest type found in the JSON ledger API.
This method returns void, and once the commands are passed the preparation stage, the Wallet provider MUST emit an txChanged event.
ledgerApi
Enables submitting requests to the validator node's JSON Ledger API associated with the account. The requests are authenticated to read as the party associated with the account.
type LedgerApiRequest = {
requestMethod: "get" | "post" | "put" | "delete" | "patch",
resource: string,
path: object | undefined,
query: object | undefined,
body: object | undefined,
headers: object | undefined
}
// responses are returned exactly according to the OpenAPI spec defining the JSON Ledger API
type LedgerApiResponse = object
Note the following assumptions about the request arguments:
requestMethod: Any HTTP method supported by the Ledger API, in all lowercase.resource: Corresponds to the path of the Ledger API operation, as defined by the OpenAPI document. This may contain string templated path parameters delimited with{}. The template is resolved by thepathrequest object.path: (optional) A map of values whose keys correspond to path parameters for a given Ledger API operation.query: (optional) A map of values whose keys correspond to additional query parameters for a given Ledger API operation.body: (optional) A map of values corresponding to the request body submitted for a given Ledger API operation.headers: (optional) Additional HTTP headers to include with the request.
accountsChanged
Carries a list of accounts (see listAccounts for type definition) that the user has access to. A wallet provider MUST at least include all accounts that changed.
statusChanged
This event results from changes made by the user related to network connectivity and/or authentication. The event carries a payload of type StatusEvent as described in the status method.
txChanged
This event is emitted along the lifecycle of a command submission (aiming to result in an executed ledger event). Depending on the change or outcome, the event payload will contain one of the following types:
// TxChangedPendingEvent
{
status: Enum[pending],
commandId: string
}
// TxChangedSignedEvent
{
status: Enum[signed],
commandId: string
payload: {
signature: string,
signedBy: string,
party: string
}
}
// TxChangedExecutedEvent
{
status: Enum[executed],
commandId: string
payload: {
updateId: string,
completionOffset: number
}
}
// TxChangedFailedEvent
{
status: Enum[failed],
commandId: string
}
Asynchronous dApp API
The Asynchronous dApp API variant is designed for server-side Wallets or Wallets deployed as remote services, where synchronous interactions are not feasible due to transport limitations (e.g., HTTP) or the need for multi-step user authorization. The async dApp API mirrors the sync dApp API in a one-to-one fashion exception for operations that require user interaction, e.g., login or transaction approval. These are decomposed into multi-step workflows.
For such multi-step operations, Wallet Providers MUST return a userUrl that directs the user to complete the required action, and MUST emit the corresponding event once the action is finalized. This variant of the dApp API enables backends and server-side applications to integrate with Canton Wallets while avoiding blocking network calls.
Overview of affected methods compared to the Synchronous dApp API above:
| Method | Output | Notes |
|---|---|---|
| connect | {<br><br>..ConnectResult, userUrl: string<br><br>} | If no prior connection is established, a `userUrl` is returned that points the user to a login facility. After a successful login, an `connected` event **MUST** be submitted. |
| prepareExecute | { userUrl: string } | A `userUrl` is returned, pointing the user to a review facility to approve or decline the signing of the submitted commands. A `txChanged` **MUST** be emitted no later than completing the `prepare` phase. |
Additional event:
| Event | Payload | Notes |
|---|---|---|
| connected | StatusEvent | Contains the same payload as `statusChanged` but is only emitted as part of the login flow. |
This variant ensures that operations that normally require synchronous blocking can proceed in a non-blocking, multi-step manner using userUrl handoffs and corresponding events.
API Evolution
This section defines how the dApp API and its associated artifacts are expected to evolve following the acceptance of this CIP.
Versioning
The API artifacts defined by this CIP (OpenRPC specifications and interface definitions) MUST be versioned. The authors of this CIP will publish the first stable version 1.0.0 of the API upon acceptance of the CIP. Published artifacts will be maintained in the /api-specs/ directory in the splice-wallet-kernel repository.
Apps SHOULD check upon connecting whether the version is recent enough to support their use of the API.
Categories of Changes
API changes fall into three categories:
Editorial Changes
Clarifications, wording updates, and other non-semantic improvements MAY be made at any time. These changes neither require a new API version nor a CIP amendment.
Non-Breaking Changes
Backward-compatible modifications – such as adding optional fields or new methods that do not affect existing behavior – MAY be introduced in coordination with other wallet providers. Such changes SHOULD result in a new minor version of the API artifacts, but do not require an amendment to this CIP.
Breaking Changes
Modifications that alter existing semantics, remove functionality, or introduce new mandatory behavior MUST be treated as breaking. Breaking changes REQUIRE an amendment to this CIP and the publication of a new minor version of the API artifacts.
Guiding Principles
The evolution of the API SHOULD prioritize:
- Long-term interoperability across independently developed wallets and dApps
- Stability of published versions
- Minimization of breaking changes
- Predictable upgrade paths for implementers
This CIP governs the lifecycle of the dApp API; any change that alters normative behavior falls under the processes defined above.
Rationale
The design of the dApp API is motivated by the need to decouple dApps from wallet implementations, reduce fragmentation, and enable a healthy multi-provider ecosystem. Prior to this CIP, dApps were forced to integrate directly with ledger endpoints, bespoke wallet connectors, or environment-specific SDKs. This created tight coupling between dApps, network transports, and key-management architectures, preventing interoperability and limiting user choice.
Alignment with existing ecosystem standards
A major design goal was to converge with established practices in other decentralized ecosystems. The dApp API adopts the Provider abstraction from EIP-1193 and the error semantics defined by EIP-1474. This ensures that developers already familiar with Ethereum’s provider model can integrate with Canton using well-understood concepts such as:
- A single Provider object exposed to the client
- A generic
request(method, params)interface - Event-based state updates (
statusChanged,accountsChanged, etc.) - A standardized, machine-readable error model
The dApp API intentionally mirrors this structure so that wallets and dApps can interoperate using a minimal, predictable, and widely recognized interaction pattern. At the same time, it extends these semantics to accommodate Canton-specific features such as privacy-preserving participants, multi-party workflows, synchronized settlement, and multi-validator environments.
The API can also be wrapped in a Provider object, just like the Ethereum provider model, ensuring a familiar integration pattern and smooth developer onboarding.
Transport-agnostic design
Rather than binding dApps to a specific communication channel, the CIP defines the API using OpenRPC, allowing Wallet Providers to expose the interface over any transport, such as:
postMessagefor browser extensions- HTTPS endpoints for remote custody services
- WebSockets for persistent desktop clients
- In-app bridges for mobile applications
This transport-agnostic approach was selected to accommodate the wide variety of custody models in use within Canton deployments – from browser wallets to enterprise HSM-backed signing gateways – while ensuring that all remain interoperable with the same dApp code. Established providers such as WalletConnect are also compatible with this model.
Alternative designs that use a single mandated transport (e.g., WebSockets or postMessage only) were considered but rejected because they would exclude significant wallet categories, particularly remote or server-side custody solutions.
Interoperability with the JSON Ledger API
The dApp API does not replace the JSON Ledger API; instead, it extends it. The inclusion of the ledgerApi method allows dApps to utilize the existing ledger API without bypassing the Wallet Provider. This avoids duplicating APIs and preserves the Ledger API as the canonical source of truth for ledger semantics. The WebSocket endpoints defined in the Ledger API are not included within the scope of the ledgerApi method in this specification, as proxying WebSockets over JSON-RPC is technically too complex. Supporting them might be done as a future extension to the standard.
Direct versus proxied Ledger API access
For dApp API providers that expose a server-side implementation of the API using the asynchronous specification, scalability concerns arise when proxying Ledger API read requests. While the Ledger API server is optimized for high-throughput, multi-client read access, wallet providers should not be expected to offer comparable performance when proxying such requests through an additional abstraction layer. Therefore, a dApp connected to such a server-side provider MUST perform read operations directly against the Ledger API, using the network access parameters (Ledger API endpoint and access token) when supplied by the provider.
Minimal but sufficient scope
During design discussions, another alternative was to expose rich, multi-step transaction flows or high-level Canton workflow abstractions. These were rejected in favor of a minimal, low-level operation set:
- Connect
- Sign
- Submit commands
- Read from the ledger
- Observe account and network changes
This keeps Wallet Providers lightweight and avoids imposing application-level conventions that vary widely across Canton deployments.
Topology-related capabilities
The dApp API does not currently expose topology-related capabilities, such as party allocation, directly to dApps. This decision was made to separate concerns and reduce the attack surface: topology operations typically require elevated privileges and administrative access that should not be exposed to standard dApps.
Instead, these capabilities are delegated to the Wallet Provider, which has access to the administrative APIs (with the necessary permissions) and can perform such operations securely.
This approach also aligns with the broader goal of keeping the dApp API forward-compatible and focused on ledger interaction, signing, and user-centric workflows, leaving provider-specific administrative tasks out of the standard interface.
Supporting Client-side and Remote Wallets
The dApp API is split into synchronous and asynchronous variants to address the distinct operational constraints of different Wallet deployments. Client-side Wallets, such as browser extensions, can handle synchronous interactions because the user is directly available to approve actions in real time. Remote or server-side Wallets, in contrast, face transport limitations (e.g., HTTP request timeouts) and cannot block while waiting for user input. By introducing an asynchronous variant, the dApp API allows server-side Wallets to return a userUrl for required actions and emit events once the user completes them. This design preserves security guarantees, supports scalable back-end deployments, and ensures that dApps can interact uniformly with both client-side and remote Wallets without changing application logic.
Community consensus and objections
Throughout discussions on cip-discuss, several concerns were raised and addressed:
- Risk of over-abstracting ledger semantics → mitigated by exposing the existing Ledger API via
ledgerApiinstead of redefining it - Fear that multiple transports would fragment the ecosystem → resolved by defining a single canonical OpenRPC specification, transport-agnostic but semantically precise.
- Concerns about browser vs. enterprise wallets diverging → addressed via the minimal operation set and remote API variant, enabling both models to conform to the same interface
The resulting design reflects broad agreement among wallet providers, SDK authors, and dApp developers across the Canton ecosystem.
References
The following resources provide normative specifications, reference implementations, and SDKs compatible with the dApp API. The specifications listed below will become normative with the v1.0.0 release, while the client and server packages serve as reference implementations to illustrate correct usage.
Specifications
Normative definition of the synchronous and asynchronous dApp API
- dApp API Specification (Synchronous): OpenRPC JSON
- dApp API Specification (Asynchronous): OpenRPC JSON
Specification for the Ledger JSON API. Refer here to determine request/response bodies for the ledgerApi dApp API method. Note that the spec linked below is for Canton 3.5.x. Definitions may vary between Canton versions, so use the specification corresponding to the version supported by the node provider.
- Ledger JSON API (v3.5): OpenAPI YAML
Reference Implementations
- Provider: NPM package | GitHub source
- dApp API Client (Synchronous): NPM package | GitHub source
- dApp API Client (Remote / Async): NPM package | GitHub source
- dApp API Server (Remote / Async): NPM package | GitHub source
SDKs compatible with the dApp API
- Canton dApp SDK (Digital Asset) NPM package | GitHub source
- Console Wallet dApp SDK (PixelPlex) NPM package
Change log
- 2025-11-28: Initial draft of the proposal.
- 2026-01-29: CIP Approved.
- 2026-03-10: Amendment to the
ledgerApimethod specification.