# Integrate a Conduit \[Build an earn experience on top of Railnet — deposits, withdrawals, balances, and reporting]

Show balances, take deposits and withdrawals, preview amounts with fees, and track positions through the Railnet API. Railnet deploys Conduits today, so contact us to get yours; see the [Conduit reference](/developers/contracts/conduit) for what deployment sets up.

:::tip
Prefer not to build the interface yourself? The [Railnet widget](/conduits/widget) gives you deposits, withdrawals and positions over your Conduits, in your brand, as a hosted page or an embed.
:::

You need the Conduit address on your target network, an Ethereum JSON-RPC provider, and the Railnet API endpoint (`https://graphql.railnet.org/query` in production, `https://graphql.staging.railnet.org/query` on staging).

### Test Conduit

Build against this one while you wait for your own. It is the default shape of a Railnet product, one Conduit wrapping one Vehicle with no asset manager involved, and it accepts both deposits and redeems. The Conduit lives at `0xcf1f211e04b1cbe452e78dac94e9d5db40a00ed3` on Base (chain ID `8453`), takes USDC, and wraps a `MorphoBlueVehicle` at `0x642c0bfa15b0dd3cc705f1737d4abad0714e1bb1` on the Morpho USDC/USDe market, connected through the [Morpho Blue adapter](/yield-sources/morpho-blue). It is indexed on staging. Call `ready()` to confirm it is accepting operations before you test against it.

## Read Conduit state

A Conduit exposes view functions you call directly on-chain for product info, total value, and balances.

```solidity [Solidity]
address asset = conduit.asset();            // underlying asset, e.g. USDC
IVehicle vehicle = conduit.getVehicle();    // the Vehicle it wraps
bool isReady = conduit.ready();             // enabled and accepting operations

uint256 tvl = conduit.totalAssets();
uint256 supply = conduit.totalSupply();
uint256 userShares = conduit.balanceOf(userAddress);

// Share price: assets per share, no fees applied
Asset memory shareValue = conduit.convert(
    Asset({asset: address(conduit), value: 1e18}),  // 1 share
    true                                            // shares → assets
);
```

## Preview an operation

`estimate()` returns what the user receives after all fees (deposit, redeem, management, performance). Use it for transaction previews, and `convert()` for display-only share-to-asset conversion.

```solidity [Solidity]
// cShares for 100 USDC
Asset memory estimated = conduit.estimate(
    Asset({asset: address(usdc), value: 100e6}),
    Mode.DEPOSIT,
    EstimationType.OUTPUT
);

// USDC for burning 50 cShares
Asset memory redeemEstimate = conduit.estimate(
    Asset({asset: address(conduit), value: 50e18}),
    Mode.REDEEM,
    EstimationType.OUTPUT
);
```

## Deposit

The user approves the Conduit, then you create the deposit. The Conduit binds each query id to its creator: `query.salt` must equal `keccak256(abi.encode(msg.sender, sourceSalt))`, otherwise `create` reverts `InvalidQuerySalt`. This stops anyone else occupying the id your user's query would get.

```solidity [Solidity]
IERC20(usdc).approve(address(conduit), amount);

bytes32 sourceSalt = keccak256(abi.encode("deposit", nonce));

Query memory query = Query({
    owner: address(conduit),
    receiver: address(conduit),
    input: Asset({asset: address(usdc), value: amount}),
    output: Asset({asset: address(conduit.getVehicle()), value: 0}),
    mode: Mode.DEPOSIT,
    salt: keccak256(abi.encode(msg.sender, sourceSalt)),
    data: ""
});

(Id queryId, State state) = conduit.create(query, userAddress, sourceSalt);
```

The query's `owner` and `receiver` are always the Conduit; the `userAddress` argument is who receives the minted cShares.

:::warning
`output.value` is a floor enforced at the **Vehicle** output only. It ignores Conduit fees and the cShare exchange rate, so it does not bound what the user finally receives. Set it from `estimate()` if you need slippage protection, or `0` to disable it.
:::

## Redeem

`createRedeemFromConduitShares` converts the cShare amount to Vehicle shares at the current ratio and hands the assembled REDEEM query to the same `create` entrypoint. No `approve` is needed: the Conduit burns the caller's cShares through its internal ERC-20 primitives, so a withdrawal is one transaction. Build the query yourself only if you already hold a Vehicle-share figure, in which case `input.asset` must be the Vehicle address, never the Conduit's.

```solidity [Solidity]
bytes32 sourceSalt = keccak256(abi.encode("redeem", nonce));

// Minimum asset to receive; 0 disables the floor
Asset memory outputAsset = Asset({asset: address(usdc), value: minAssetsOut});

(Id queryId, State state) = conduit.createRedeemFromConduitShares(
    shareAmount,
    outputAsset,
    sourceSalt,
    userAddress
);
```

## Pending state

Your integration code is the same whatever sits behind the Conduit; only the states a query passes through differ. With a sync Vehicle (Aave V3, Morpho Blue, an ERC-4626 vault such as Morpho Vaults or Euler) a deposit or redeem settles in the same transaction. With an async Vehicle (Ethena's sUSDe cooldown, an ERC-7540 vault such as Lagoon or Centrifuge) the query enters `PROCESSING` and a keeper calls `process()` when the protocol is ready, so the user never returns for a second transaction. Show that wait: read the query state and render a pending badge until it reaches `SETTLED`. A MultiVehicle may hold a mix of both.

## Track positions and operations

Query indexed state for balances, history, and pending badges. See the [API reference](/developers/api) for conventions: addresses are indexed lowercase, so filter with `_ilike`, and every `numeric` value is a decimal string in raw on-chain units.

```graphql [GraphQL]
query UserPosition($conduit: String!, $owner: String!) {
  ConduitBalance(
    where: { conduit: { address: { _ilike: $conduit } }, owner: { _ilike: $owner } }
  ) {
    value
    conduit { symbol assetSymbol assetDecimals }
  }
}
```

Each deposit or withdrawal is a `ConduitQuery` wrapping a STEAM `Query`. Filtering out the two terminal states gives you everything still in flight:

```graphql [GraphQL]
query PendingOperations($conduit: String!) {
  ConduitQuery(
    where: {
      conduit: { address: { _ilike: $conduit } }
      _not: { query: { state: { _in: ["SETTLED", "REJECTED"] } } }
    }
    order_by: { event: { tx: { block: { number: asc } } } }
  ) {
    id
    owner
    query { mode state }
  }
}
```

## Next steps

<Cards>
  <Card title="Configure fees" description="Set management, performance, deposit, and redeem fees on your Conduit." icon="percent" to="/conduits/configure-fees" />

  <Card title="Set up compliance" description="Allowlists, blocklists, and sanctions screening." icon="shield-check" to="/conduits/compliance" />

  <Card title="Conduit reference" description="Full technical reference for the Conduit contract." icon="book" to="/developers/contracts/conduit" />
</Cards>
