> ## Documentation Index
> Fetch the complete documentation index at: https://docs.passage.coinlist.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Centrifuge Funds

> Put USDC into tokenized funds from managers like Janus Henderson and New York Life Investment Management, and redeem back to USDC. A trade settles in two steps: the user buys or sells, the fund fills the request on its schedule, and the user claims the result.

> [!NOTE]
>
> **Recommended:** render [`CheckoutContainer`](#quickstart) to buy or sell, and [`ClaimContainer`](#claiming) to claim. Both detect `centrifuge::vault` offers and run the whole flow. This page covers what is specific to Centrifuge and the levels below the components.

## What Centrifuge is

Centrifuge is an onchain asset manager and tokenization platform. Through Passage, it offers **deRWA tokens**: wrapped tokens that give economic exposure to an institutional fund and move around DeFi like any other ERC-20. The token symbol is `de` followed by the fund's ticker.

| Token | Underlying fund | Exposure |
| - | - | - |
| `deJTRSY` | Janus Henderson Anemoy Treasury Fund (JTRSY) | Short-term US Treasury bills |
| `deJAAA` | Janus Henderson Anemoy AAA CLO Fund (JAAA) | AAA-rated collateralized loan obligations |
| `deHYB` | NYLIM Anemoy US High Yield Corporate Bond Fund (HYB) | US high yield corporate bonds |

Read the list of funds from the offer catalog rather than this table. Each fund is one offer with `type: "centrifuge::vault"`, and `offer.vaultContracts` names every chain it accepts requests on. Passage supports Centrifuge on Ethereum.

Passage does not route requests straight to Centrifuge. Each offer has a **CoinList proxy** contract on every chain it supports, and every request and every claim goes through it. The proxy only accepts a transaction that CoinList signed for that wallet, so Passage builds each one, and users have to meet the offer's [requirements](https://docs.passage.coinlist.co/sdk/requirements.md) before they can buy.

## A trade settles in two steps: buy or sell, then claim

This is the main difference from Ondo and Superstate, and it works like a mutual fund off-chain. An order in a mutual fund is not filled at once at a market price. It waits for the fund's next dealing point and is filled at the price the fund calculates then, along with every other order in that batch.

A Centrifuge fund is **asynchronous** in the same way (it follows the ERC-7540 vault standard). Buying or selling sends a request, and the request does not move any tokens to the user yet. The fund fills the queue later, at a price it sets at that point, and the user then claims the result in a second transaction. Each fund's dealing days and typical time to fill are listed under [Fund schedules](https://docs.passage.coinlist.co/assets/partners/centrifuge.md#fund-schedules).

```mermaid theme={null}
flowchart LR
  subgraph Buy
    direction LR
    A1["Request<br/>user sends USDC"] --> B1["Pending<br/>queued, in USDC"] -.->|fund fills| C1["Claimable<br/>filled, in USDC"] --> D1["Claimed<br/>shares in wallet"]
  end
  subgraph Sell
    direction LR
    A2["Request<br/>user sends shares"] --> B2["Pending<br/>queued, in shares"] -.->|fund fills| C2["Claimable<br/>filled, in shares"] --> D2["Claimed<br/>USDC in wallet"]
  end
```

Solid arrows are transactions the user signs. The dashed step is the fund filling the queue, which Passage does not control. A fill can be partial, so `pending` and `claimable` can both be above zero at once.

### What this means for your UI

* **There is no order ID.** Each wallet has one pooled position per fund, per chain, per direction. A second deposit adds to the same position. You claim by wallet and direction, and a claim takes everything that is claimable.
* **Amounts before a claim are estimates.** The fund sets the price when it fills. `estimatedShares` and `estimatedAssets` use the current price. What a claim actually paid is in its `settlement`.
* **Requests cannot be cancelled.** Once a request mines, it waits in the queue until the fund fills it.
* **Only the wallet that placed the request can claim it.** The claim is signed for that wallet, and the proxy pays whoever sends it. Make sure your users come back with the same wallet.

## Quickstart

Render the checkout for the request and the claim card for the second step on the same page. Nothing Centrifuge-specific needs configuring: the proxy, share token and funding token all come from the offer.

```tsx FundPage.tsx theme={null}
"use client";

import {
  CheckoutContainer,
  ClaimContainer,
  type CheckoutWalletSelection,
} from "@passage-cl/react";
import {
  AsyncOfferDetail,
  type OfferDetail,
  type OrderBookSide,
} from "@passage-cl/react/universal";

export function FundPage({
  offer,
  wallets,
  side,
  onPortfolioChange,
}: {
  offer: OfferDetail;
  wallets: CheckoutWalletSelection;
  side: OrderBookSide; // "buy" places a deposit, "sell" a redemption
  onPortfolioChange: () => void;
}) {
  return (
    <>
      <CheckoutContainer
        // side is read once at mount, so it belongs in the key
        key={`${offer.id}:${side}`}
        offer={offer}
        chain="ethereum_mainnet" // must be a chain in offer.vaultContracts
        side={side}
        wallets={wallets}
        config={{
          "centrifuge::vault": {
            onOrderPlaced: (order) => console.log("Request queued", order.txHash),
          },
        }}
      />

      {AsyncOfferDetail.is(offer) && (
        <ClaimContainer
          key={offer.id}
          offer={offer}
          wallets={wallets}
          config={{
            "centrifuge::vault": {
              onClaimed: () => onPortfolioChange(),
            },
          }}
        />
      )}
    </>
  );
}
```

What the user sees:

**1. Amount**

They enter USDC to deposit, or shares to redeem. The checkout checks their balance and the fund's status. If buying or selling is paused, the amount step is blocked with an explanation. Claiming is never paused. For a deposit, confirming the amount also asks for the approval, if the proxy's allowance does not already cover it, and builds the request, so the review can show its fee.

**2. Review and sign**

The review shows the amount and, for a deposit, the CoinList fee. **Place** sends the request. A redemption asks for its approval here instead, then sends the request. The confirmation says the order is placed and that the user comes back to claim it, not that the trade is done.

**3. Claim, later**

`ClaimContainer` lists each wallet's pending and claimable amounts, read from the chain. When something is ready, the row leads with the estimated amount the user will receive and shows a **Claim** button.

> [!TIP]
>
> Users may not come back to the page with the claim card on their own. Show that a claim is ready wherever they will see it, such as a badge in your navigation or a line in their portfolio. `useCentrifugeClaimPositions` reads every wallet's position and keeps polling while anything is pending, so you can show the badge when any position has a claimable amount above zero.

## Fund status

The proxy can stop accepting requests in either direction, and Centrifuge sends no notice when it does. The checkout polls the status every 15 seconds while it is mounted. If you build your own UI, poll `getVaultStatus` or use `useCentrifugeVaultStatus`. It is free to call.

| State | Meaning |
| - | - |
| `active` | The proxy accepts new requests in this direction. |
| `paused` | Temporarily closed. It can reopen without any change on your side. |
| `stopped` | Closed for good on this chain. The offer would need a new proxy. |

Status is reported separately for `deposit` (buy) and `redeem` (sell), so a fund can take redemptions while it refuses deposits. **Status never blocks a claim:** anything already filled can be claimed whatever either direction says.

> [!WARNING]
>
> `active` describes CoinList's proxy, not the Centrifuge vault behind it. It means the proxy will accept a request. It does not promise when, or whether, the fund will fill it.

## Buying and selling

Both directions use the same offer, the same proxy and the same two-step shape. They differ in which token moves in and out.

| | Buy (`side: "buy"`) | Sell (`side: "sell"`) |
| - | - | - |
| User spends | USDC | Fund shares, such as `deJTRSY` |
| Approval covers | USDC, to the proxy | Shares, to the proxy |
| Position tracked in | USDC | Shares |
| Claim pays | Shares | USDC |
| Checked before Passage signs | The offer's requirements, then a wallet screen | A wallet screen. A sale only returns shares a deposit already let through, so requirements are not checked again |
| API endpoint | `POST /v1/centrifuge/vault/invest` | `POST /v1/centrifuge/vault/sell` |

A deposit carries a CoinList fee, currently 0.5% on every fund. The fee is taken out of the deposit, not added on top: the approval covers exactly `spendInputAmount`, `fee` is the part of it CoinList keeps, and the rest enters the queue. `CheckoutContainer` shows it in the review's CoinList fee row, and the total stays the amount the user entered. A redemption carries no fee. The `fee` field is new in v0.15.1.

## Claiming

`ClaimContainer` covers this for you. Under it, a claim works like a request: the SDK asks the API to build it, the user's wallet sends it, and the SDK waits for it to mine. The claim carries a CoinList signature with a deadline, so it is built when the user clicks **Claim** and sent straight away.

* A claim collects the whole claimable amount for one direction. There is no partial claim.
* Placing a new deposit request first claims anything already claimable. So a claim can find nothing left and return `nothing-to-claim`. Treat that as already done and refresh the position.
* A request or claim in the same direction between building a claim and sending it makes the claim stale (`claim-expired`). Building it again fixes it.
* If the proxy refuses to sign (`vault-refused`), one retry can help, as the API asks. The claim card offers **Retry** once, and every retry builds the claim again.
* After a claim mines, `settlement` holds what it moved: shares paid for a deposit, USDC paid for a redemption.

## Build your own UI

Use the hooks for the SDK's logic behind your own markup (L2), or call `passage.centrifuge` directly for no React at all (L1). Both are `@alpha`.

```tsx MyFundBuy.tsx theme={null}
import {
  useCentrifugeBuyCheckoutViewModel,
  type CheckoutWalletSelection,
} from "@passage-cl/react";
import type { CentrifugeVaultOfferDetail } from "@passage-cl/react/universal";

export function MyFundBuy({
  offer,
  wallets,
}: {
  offer: CentrifugeVaultOfferDetail;
  wallets: CheckoutWalletSelection;
}) {
  const { state, onEvent } = useCentrifugeBuyCheckoutViewModel({
    offer,
    chain: "ethereum_mainnet",
    wallets,
  });
  // Render each state.type with your own components, switching on it
  // exhaustively, and send the user's actions to onEvent.
  void onEvent;
  return <p>{state.type}</p>;
}
```

| Hook | What it drives |
| - | - |
| `useCentrifugeBuyCheckoutViewModel` | The deposit checkout |
| `useCentrifugeBuyTransaction` | A built deposit request on its own, with its fee. Never polls: call `refetch` for a fresh one |
| `useCentrifugeSellCheckoutViewModel` | The redemption checkout |
| `useCentrifugeVaultClaimViewModel` | The claim card |
| `useCentrifugeVaultStatus`, `useCentrifugeVaultStatuses` | The status gate, polled |
| `useCentrifugeClaimPositions` | Each wallet's pending and claimable amounts, polled while anything is pending |

At L1, the same deposit with no React:

```ts theme={null}
const symbol = AssetSymbol("deJTRSY");
const chain = "ethereum_mainnet";

// 1. Check the gate. Free to poll.
const status = await passage.centrifuge.getVaultStatus({ symbol, chain });
if (status.deposit !== "active") return;

// 2. Approve, build and broadcast the request. Never throws.
const result = await passage.centrifuge.buy({
  wallet,
  symbol,
  chain,
  vaultContracts: offer.vaultContracts,
  tokenAddress: TOKEN_REGISTRY.usdcAddress(chain),
  amount, // BlockchainAmount, in USDC base units
  offerOptionId: optionId, // the option the user was shown
});
if (result.type === "error") return showError(result.error.step);

// 3. Later: read the position from the chain.
const resolved = resolveCentrifugeVaults(offer, lookupToken).find(
  (entry) => entry.type === "resolved" && entry.vault.chain === chain,
);
if (resolved?.type !== "resolved") return;
const position = await passage.centrifuge.readDepositPosition({
  vault: resolved.vault,
  walletAddress: wallet.address,
});
if (position.claimable.raw > 0n) showClaimButton();
```

Pass the `offerOptionId` of the option the user was shown. It is required since v0.15.0, even on an offer with one option, because the API refuses a deposit on an offer with several options rather than pick one. A redemption takes none, since it runs no option's requirements.

To claim, call `passage.centrifuge.claim` with the wallet, the share token `symbol`, the resolved `vault` and the `direction`.

### The passage.centrifuge namespace

| Method | What it does | Where |
| - | - | - |
| `getVaultStatus` | The proxy's `deposit` and `redeem` state. Free to poll. | Client and server |
| `readDepositPosition`, `readRedeemPosition` | A wallet's pending, claimable and estimated amounts, read from the proxy over RPC. | Client and server |
| `buildBuyTransaction`, `buildRedeemTransaction` | Signed calldata for one request. Call once, when the user confirms. | Client and server |
| `buildClaimTransaction` | Signed calldata for one claim. Call once, when the user claims. | Client and server |
| `buy`, `sell` | Approve, build and broadcast a request in one call. | Browser only |
| `prepareBuy`, `prepareSell`, `placeRequest` | The same two halves separately, if you want to show the calldata before sending. | Browser only |
| `claim` | Builds the claim, sends it, and waits for it to mine. | Browser only |

Flows return a result tagged `success` or `error` instead of throwing, the same as the other providers. API-backed methods throw `NotAuthenticatedError` without a session.

## REST API

For a server or a non-JavaScript client. All endpoints need an authenticated user. Amounts are uint256 decimal strings in the token's smallest unit, and timestamps are ISO 8601.

### Status

`GET /v1/centrifuge/vault/status?symbol=deJTRSY&chain=ethereum_mainnet`

```json 200 OK theme={null}
{
  "object": "centrifuge_vault_status",
  "symbol": "deJTRSY",
  "chain": "ethereum_mainnet",
  "deposit": "active",
  "redeem": "active",
  "flags": [],
  "can_order": true,
  "reasons": [],
  "as_of": "2026-10-26T14:32:05Z"
}
```

### Requests

`POST /v1/centrifuge/vault/invest` builds a deposit request. `POST /v1/centrifuge/vault/sell` takes the same body, with `amount` in shares.

```json Request theme={null}
{
  "symbol": "deJTRSY",
  "chain": "ethereum_mainnet",
  "wallet_address": "0x4b2e...9a1f",
  "amount": "250000000"
}
```

```json 200 OK theme={null}
{
  "action": "broadcast_transaction",
  "to": "0x7c1d...e04b",
  "data": "0x8f4e1a2c...",
  "expires_at": "2026-10-26T14:47:05Z",
  "spend_input_amount": "250000000",
  "spend_input_decimals": 6
}
```

Broadcast `to` (the offer's proxy) and `data` unchanged, from the same `wallet_address`. The signature names that wallet, and expires at `expires_at`.

### Claims

`POST /v1/centrifuge/vault/claim` builds the claim for a filled deposit, and `POST /v1/centrifuge/vault/redeem` the claim for a filled redemption. Both take `symbol`, `chain` and `wallet_address`, and claim everything claimable.

```json 200 OK theme={null}
{
  "action": "broadcast_transaction",
  "to": "0x7c1d...e04b",
  "data": "0x3a9f7b10...",
  "expires_at": "2026-10-26T14:47:05Z",
  "claim_amount": "250000000",
  "claim_decimals": 6
}
```

`claim_amount` is what the claim collects, in the unit the position is tracked in: USDC for a deposit, shares for a redemption. What it pays out is only known once it mines. A wallet with nothing claimable gets `nothing_to_claim`.

Positions are not served by the API. Read `pendingDepositRequest` and `claimableDepositRequest` (or the redeem pair) on the proxy directly.

## Errors

### When Passage declines to build a request

Returned as a `build-refused` step, with a reason the user can act on.

| Reason | Meaning | What to tell the user |
| - | - | - |
| `requirements-not-met` | The offer's requirements, such as KYC, are incomplete. Deposits only. | Finish the offer's requirements first. |
| `insufficient-balance` | The wallet holds less than the amount. | Lower the amount or add funds. |
| `direction-paused` | The proxy has paused buying or selling. | Try again later. |
| `direction-stopped` | The proxy has stopped buying or selling for good. | This fund is no longer available here. |
| `vault-refused` | Centrifuge itself would reject the request. | Try a different amount, or contact support. |
| `no-proxy-on-chain` | The offer has no proxy on this chain. | Switch to a supported chain. |

### When a request fails

| Step | Meaning | Safe to retry? |
| - | - | - |
| `request-expired` | The signed calldata went stale before it was sent. | Yes. Placing again builds a fresh one. The SDK's deposit review rebuilds it once by itself, with no new approval. |
| `request` | The wallet never sent it, for example the user rejected it. | Yes |
| `request-unconfirmed` | Sent, but the wait for its receipt failed. Carries `txHash`. | **No.** It may still mine. Check the hash first, or you can queue a second request. |
| `request-reverted` | Mined and reverted. | Yes, after fixing the cause |

### When a claim fails

| Step | Meaning | Safe to retry? |
| - | - | - |
| `nothing-to-claim` | Nothing is claimable: the position was already claimed. | No need. Refresh the position. |
| `build-refused` | The API refused to build the claim: `vault-refused` or `no-proxy-on-chain`. | Once, for `vault-refused` |
| `build-transaction` | Any other build failure, such as the network or a blocked wallet. | Yes |
| `claim-expired` | The claim went stale before it mined. | Yes. Claiming again builds a fresh one. |
| `broadcast` | The wallet did not send it. | Yes |
| `unconfirmed` | Sent, but the wait for its receipt failed. Carries `txHash`. | **No.** Check the hash first. |
| `reverted` | Mined and reverted. | Yes, after fixing the cause |

See [Errors](https://docs.passage.coinlist.co/sdk/errors.md#centrifuge-errors) for the full error types.

## FAQ

> **What does my user own?**
>
> A deRWA token issued by Centrifuge that wraps a share of the underlying fund. That gives economic exposure to the fund's returns, not direct ownership of its assets. The Janus Henderson Anemoy funds are run through Anemoy Capital SPC, a segregated portfolio company regulated in the British Virgin Islands.

> **How long until a request can be claimed?**
>
> It depends on the fund. See [Fund schedules](https://docs.passage.coinlist.co/assets/partners/centrifuge.md#fund-schedules) for each fund's dealing days and typical time to fill. A fill can be partial. Show the user the pending amount and let them come back, rather than holding them on a spinner.

> **Why did the user get a different number of shares than the estimate?**
>
> The estimate uses the price when the position was read. The claim settles at the price the fund set when it filled the request, which is the same for everyone in that batch. The exact figures are in the claim's `settlement`.

> **Can a user cancel a request?**
>
> No. Once a request mines, it stays queued until the fund fills it.

> **Does the yield show up as more tokens?**
>
> No. The fund's return goes into the token's price, not the user's token balance, so each token is worth more USDC over time. When the user redeems, the claim pays USDC at the price on the fill date, so they get back more USDC than they put in, less any fall in the fund's value.
>
> To show yield, value the user's tokens at the current token price and show the change since they bought.

> **Can users transfer the tokens or use them in DeFi?**
>
> Yes. deRWA tokens move freely between wallets and into DeFi protocols. Users can also sell them on DeFi protocols that support them, but liquidity there can be limited.
>
> Requesting or redeeming through Passage still needs a wallet that qualifies for the offer, so someone who receives tokens from another wallet can only redeem through Passage if they meet the offer's requirements.

> **Which jurisdictions are excluded?**
>
> Centrifuge funds are not available in the United States and its territories, 22 other countries, or 5 regions of Ukraine. The full list is on the [Centrifuge](https://docs.passage.coinlist.co/assets/partners/centrifuge.md#restricted-jurisdictions) issuer page.

> **What does it cost?**
>
> CoinList charges 0.5% on deposits, taken out of the deposit amount. Redemptions and claims carry none. The fund also charges its own management fee, which is reflected in the token price. See [Fund schedules](https://docs.passage.coinlist.co/assets/partners/centrifuge.md#fund-schedules).

## Next steps

> **[Checkout](https://docs.passage.coinlist.co/sdk/checkout.md)**
>
> How `CheckoutContainer` picks a provider, and the props every checkout shares.

> **[Requirements](https://docs.passage.coinlist.co/sdk/requirements.md)**
>
> The KYC and eligibility steps a user completes before their first deposit.

> **[Wallets](https://docs.passage.coinlist.co/sdk/wallets.md)**
>
> Embedded and external wallets, and skipping the wallet step.

> **[Errors](https://docs.passage.coinlist.co/sdk/errors.md)**
>
> Every flow's error steps, and which ones are safe to retry.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.