npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@circle-fin/provider-cctpx

v1.0.2

Published

Circle's CCTPx provider for non-USDC cross-chain token transfers via the CrossChainTokenService

Readme

CCTPx Bridging Provider

npm version TypeScript License Discord

Circle's CCTPx provider for Bridge Kit

Cross-chain transfers of non-USDC tokens via the CrossChainTokenService.

Table of Contents

Overview

The CCTPx Bridging Provider is a strongly-typed implementation of Circle's CCTPx protocol. Where the CCTPv2 provider moves native USDC, CCTPx moves non-USDC tokens across chains through the CrossChainTokenService. Which tokens and routes are available is driven by the CCTPx token registry; KNOWN_TOKEN_SYMBOLS lists the symbols the provider resolves on the caller's behalf.

A token is identified by an opaque protocol-native identifier (a bytes32 token id), not by a symbol tied to a single chain address. The provider resolves a known symbol ('cirBTC' / 'wETH' / 'EURC') to that id internally, validates the route against the CCTPx token registry, fetches a server-signed fee quote, and runs the full approve → transfer → attestation → relayer-forward flow.

While primarily designed to power the Bridge Kit, the provider can be used directly in applications that need fine-grained control.

Installation

npm install @circle-fin/provider-cctpx
# or
yarn add @circle-fin/provider-cctpx

Note: This provider is included by default with the Bridge Kit. Install it directly only if you need a custom CCTPx integration.

Usage

With Bridge Kit (recommended)

The Bridge Kit registers the CCTPx provider by default, after the CCTPv2 provider. A bare 'USDC' token continues to route through CCTPv2; a known CCTPx symbol or bytes32 token id routes through CCTPx. The example below uses a testnet route (Ethereum_Sepolia → Arc_Testnet):

import { BridgeKit } from '@circle-fin/bridge-kit'

const kit = new BridgeKit()

const result = await kit.bridge({
  from: { adapter: sourceAdapter, chain: 'Ethereum_Sepolia' },
  to: { adapter: destAdapter, chain: 'Arc_Testnet' },
  amount: '0.05',
  token: 'wETH',
})

if (result.state === 'success') {
  console.log(result.steps.map((step) => step.name))
}

Pass 'cirBTC', 'wETH', 'EURC', or a 32-byte hex token id (0x plus 64 hex characters). The CCTPx token registry decides whether that id is routable. Unknown symbols, short/malformed ids, and { provider, id } objects throw at bridge / estimate. See Supported Chains & Tokens for the routable chains.

Estimating a transfer

estimate returns a server-signed fee quote alongside a single-entry fees[] summary. The shared result type leaves quote opaque, so narrow it to this provider's QuoteEnvelope to read the breakdown. CCTPx fees are quote-driven, so the gas-fee estimate is empty:

import type { QuoteEnvelope } from '@circle-fin/provider-cctpx'

const estimate = await kit.estimate({
  from: { adapter: sourceAdapter, chain: 'Ethereum_Sepolia' },
  to: { adapter: destAdapter, chain: 'Arc_Testnet' },
  amount: '0.05',
  token: 'wETH',
})

// `quote` is opaque on the shared result type. Narrow it to this provider's
// envelope to read the fee breakdown.
const quote = estimate.quote as QuoteEnvelope | undefined
console.log(quote?.totalAmount, estimate.fees[0]?.token)

Pass that quote back into bridge to reuse it. CCTPx reuses a quote while it is unexpired and matches the requested speed; otherwise it fetches a fresh one and adds a QUOTE_NOT_REUSED warning to result.warnings.

Direct provider usage

supportsRoute takes two chain definitions. The published chain definitions are re-exported from @circle-fin/bridge-kit (which bundles this provider), so a direct integrator imports them from there:

import { CCTPXBridgingProvider } from '@circle-fin/provider-cctpx'
import { EthereumSepolia, ArcTestnet } from '@circle-fin/bridge-kit'

const provider = new CCTPXBridgingProvider()

// Route checks read the CCTPx token registry (latency-bounded, cached).
const canRoute = await provider.supportsRoute(
  EthereumSepolia,
  ArcTestnet,
  'wETH',
)

A route check resolves to true/false for a definitive answer. If the token registry is unreachable with a cold cache, supportsRoute instead throws a retryable KitError (SERVICE_ROUTE_CHECK_UNAVAILABLE) — "couldn't check", distinct from a definitive false — so a direct integrator should be ready to catch it. The Bridge Kit's routing layer does this and surfaces a retryable route-check error rather than a misleading "unsupported route".

Public API

The Bridge Kit re-exports the CCTPx surface most integrators need:

import {
  CCTPXBridgingProvider,
  KNOWN_TOKEN_SYMBOLS,
  type CCTPXBridgingProviderConfig,
  type BridgeToken,
  type CCTPXTokenId,
  type CCTPXRouteToken,
  type KnownTokenSymbol,
  type CCTPXChainConfig,
  type ChainDefinitionWithCCTPX,
} from '@circle-fin/bridge-kit'
  • CCTPXBridgingProvider — the provider class (registered by default).
  • KNOWN_TOKEN_SYMBOLS — the runtime list of symbols resolvable without a bytes32 id (['cirBTC', 'wETH', 'EURC']).
  • CCTPXBridgingProviderConfig — the constructor options, for wiring a provider instance explicitly instead of relying on the default registration.
  • BridgeToken — the Bridge Kit token input: 'USDC', a known CCTPx symbol, or a bytes32 CCTPx token id.
  • CCTPXTokenId — the bytes32 token-id type.
  • CCTPXRouteToken — what a route check accepts: a CCTPXTokenId or a KnownTokenSymbol. Which ids are routable is decided by the IRIS registry at call time, so this is a shape, not a validity guarantee.
  • KnownTokenSymbol — the union of those symbols, derived from the list above so the type and the runtime set never drift.
  • CCTPXChainConfig — the per-chain CCTPx configuration shape (the CrossChainTokenService address and supported tokens).
  • ChainDefinitionWithCCTPX — a narrowed ChainDefinition type for chains confirmed to carry a CCTPx deployment (a ChainDefinition whose cctpx config is present), not a runtime guard.

The provider package additionally exports the registry fetcher, the fee-quote fetcher, the action-map type (CCTPXActions), the forward-state and attestation-status unions, and the IRIS API path constants. See the package entry point for the full surface.

Supported Chains & Tokens

Call provider.getSupportedChains() for the current set instead of a static list — it returns every chain whose definition in @core/chains (core/chains/src/definitions) carries a cctpx.serviceAddress (a deployed CrossChainTokenService), the same signal isCCTPXSupported reports. A chain becomes CCTPx-eligible as soon as its definition gains that field.

Eligibility is per chain; routability is per token. A route is accepted only when both endpoints are EVM chains on the same network, both carry a CCTP domain and a CrossChainTokenService deployment, and the registry lists the token with a deployment on both endpoints' domains. An eligible pair of chains is therefore not by itself a usable route for a given token.

Known symbols: the symbols listed in KNOWN_TOKEN_SYMBOLS. A symbol resolves to one canonical bridge, so it routes only between the chains that bridge is registered on; a symbol that maps to more than one bridge resolves to the canonical one. Any registry-listed token — including a non-canonical bridge for a known symbol — routes when passed by its bytes32 id.

The IRIS API

The provider talks to Circle's public IRIS API for five things:

  • Token registry — GET /v2/cctpx/tokens lists the CCTPx tokens and their per-chain deployments. Route checks read this (cached, stale-while-revalidate); provider.refreshTokenRegistry() forces a refresh.
  • Fee quote — a POST to the CCTPx quote endpoint returns a server-signed quote that the on-chain transfer carries. Quotes expire.
  • Fast-burn allowance — GET /v2/cctpx/allowances reports the remaining fast-burn allowance per token. A FAST transfer probes this before signing and degrades to SLOW when the remaining allowance cannot carry the transfer amount or when the probe itself fails; see Observability for the signals that fire.
  • Attestation and forward state — GET /v2/messages/{sourceDomain} returns the signed attestation for the source transfer once it is available. The destination leg polls the same endpoint a second time for the relayer's forwardState; see Troubleshooting for that vocabulary.
  • Re-attestation — POST /v2/reattest/{nonce} asks IRIS to re-sign a message whose attestation never arrived. provider.reAttest(source, txHash) is the operator escape hatch: it resolves the nonce via the attestation endpoint, posts the re-attest request, then re-polls until the attestation value actually changes.

Hosts: production is https://iris-api.circle.com; the sandbox is https://iris-api-sandbox.circle.com. The provider selects the host per route from the source chain's network (testnet → sandbox, mainnet → production). Both can be overridden via CCTPXBridgingProviderConfig.

Observability

The provider emits on three channels at each of the sites below — a log line, an events.emit({ name, data }) event, and a metrics.counter(name).inc() increment. All three default to no-ops, so emissions are dropped unless you wire a runtime backend.

Wiring the runtime backends

Pass a runtime with your backends via the kit's invocation metadata, or construct the provider directly with logger / events / metrics:

import { CCTPXBridgingProvider } from '@circle-fin/provider-cctpx'

// Node — e.g. pino logs + an OpenTelemetry counter + your event bus.
const provider = new CCTPXBridgingProvider({
  logger: { warn: (msg, fields) => pino.warn(fields, msg) /* ...rest */ },
  metrics: { counter: (name) => ({ inc: () => otelCounter(name).add(1) }) },
  events: { emit: (event) => myBus.publish(event.name, event.data) },
})
// Browser — e.g. forward warnings to Sentry and events to Datadog RUM.
const provider = new CCTPXBridgingProvider({
  logger: {
    warn: (msg, fields) => Sentry.captureMessage(msg, { extra: fields }),
  },
  events: { emit: (event) => datadogRum.addAction(event.name, event.data) },
})

The kit threads its runtime through the bridge flow, so wiring Runtime.{ logger, events, metrics } once captures every site below.

Emission sites

Each emission site fires one or more of the signals below. The registry and fee-quote sites additionally emit a higher-priority cctpx.registry.invalidpayload / cctpx.feequote.invalidpayload signal when a 200 OK body can't be parsed or validated.

| Event name | Metric | When it fires | Action | | ---------------------------- | -------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | cctpx.registry.failed | cctpx.registry.errors | The token registry could not be read and the cache was cold. | Check IRIS reachability; the route check throws a retryable error until the registry is readable. | | cctpx.feequote.failed | cctpx.feequote.errors | A fee quote could not be fetched from IRIS. | Check IRIS reachability; the quote fetch fails with a directed error until IRIS is readable. | | cctpx.quote.refetched | cctpx.quote.refetched | A signed quote was stale at submit time and was transparently re-fetched. | Informational; the flow rebuilds the calldata. Repeated firing suggests slow signing/submission. | | cctpx.allowance.failed | cctpx.allowance.errors | The fast-burn allowance probe could not be read (network / HTTP / bad body). | The probe degrades the request to SLOW (the safe choice); check IRIS reachability. | | cctpx.allowance.degraded | cctpx.allowance.degraded | A probe failure forced a FAST request to fall back to SLOW. | Informational; the transfer proceeds at SLOW. Pairs with cctpx.allowance.failed. | | cctpx.fastburn.unavailable | cctpx.fastburn.degraded | Fast-burn was not confirmed available, so a FAST request degraded to SLOW. | Informational; the transfer proceeds at SLOW. | | cctpx.quote.reused | cctpx.quote.reused | A caller-supplied quote passed the reuse checks and was used verbatim. | Informational; logged at debug. No fresh quote was fetched. | | cctpx.quote.reuserejected | cctpx.quote.reuserejected| A caller-supplied quote could not be reused, so a fresh quote was fetched. | Check the quote's freshness and speed; the transfer proceeds on the fresh quote. |

Troubleshooting

  • A CCTPx route returns "unsupported" unexpectedly. The route check requires both chains to carry a CrossChainTokenService deployment and the token to be registered on both domains. If cctpx.registry.failed fired, IRIS was unreachable with a cold cache — the route check throws a retryable SERVICE_ROUTE_CHECK_UNAVAILABLE error (not a definitive false) until the registry can be read. Call provider.refreshTokenRegistry() to force a fresh read on a running instance.

  • A quote keeps re-fetching (cctpx.quote.refetched). Quotes expire; a stale quote at submit time is re-fetched transparently. Frequent re-fetches usually mean signing or submission is slow relative to the quote TTL.

  • A FAST transfer ran at SLOW (cctpx.fastburn.unavailable). Fast-burn could not be confirmed available — either the remaining allowance was short or the probe failed — so it degraded to SLOW. This is expected behaviour, not an error.

  • A FAST transfer ran at SLOW after a probe error (cctpx.allowance.failed / cctpx.allowance.degraded). The fast-burn allowance probe could not be read (IRIS unreachable, a non-OK status, or an unusable response body), so the request degraded to SLOW. An over-allowance FAST transfer is not rejected on-chain — it settles at finality with the PRE_FINALITY premium already paid — so when the remaining allowance is unknown, SLOW is the correctly priced choice. Check IRIS reachability if the probe keeps failing.

  • The destination forward stalls. If the destination leg never reaches a terminal state — CONFIRMED or COMPLETE carrying a forwardTxHash — the transfer is waiting on Circle's relayer to execute the CCTPx message; the poll continues until it does. A terminal FAILED surfaces to the caller with any forwardErrorCode / forwardErrorDetails. The full forward-state vocabulary is PENDING | SENT | CONFIRMED | COMPLETE | FAILED; the terminal states are CONFIRMED and COMPLETE (once a forwardTxHash is reported), and FAILED. bridge() resolves at whichever it reaches first, rather than holding for COMPLETE — the delivery transaction is on chain at CONFIRMED, and COMPLETE only adds destination finality, which IRIS can report minutes later. The returned forwardState says which of the two the poll saw.

  • A custom fee was requested on a CCTPx route. Custom fees are a USDC-only feature: the fee is collected on top of the transfer amount and split through CCTPv2's USDC flow, so it cannot apply to a CCTPx (non-USDC) token. Requesting one — through the kit-wide policy or a per-call config.customFee — is rejected with INPUT_VALIDATION_FAILED before anything is submitted, on estimate as well as bridge. Bridge USDC, or drop the fee for that route.

  • feePayment: 'destination' was passed. CCTPx prepays its fee on the source leg, as part of the same signed quote the transfer commits to, so there is no destination-side collection to select. 'source' and an omitted value are accepted; any other value — including a near-miss such as 'Destination' — is rejected with INPUT_VALIDATION_FAILED before anything is submitted, on estimate as well as bridge.

  • useForwarder: false was passed. Every CCTPx transfer pays a forwarding fee in its quote, and that payment is what dispatches the relayer, so forwarding is bought before the transfer reaches the chain and cannot be turned off per call. Omitted and true are accepted; anything else is rejected with INPUT_VALIDATION_FAILED before anything is submitted, on estimate as well as bridge.

Development

This package is part of the App Kits monorepo.

# Build
nx build @circle-fin/provider-cctpx

# Test
nx test @circle-fin/provider-cctpx

License

This project is licensed under the Apache 2.0 License. Contact support for details.


Ready for cross-chain bridging?

Join Discord • Visit our Help-Desk

Built with ❤️ by Circle