Move money in and out

Deposit from a wallet, from a one-time address, or from another chain, and withdraw anywhere.

Money enters a private balance with a deposit and leaves with a withdrawal. Both are gasless for the user. Deposits and withdrawals are visible onchain, because money crosses the pool's edge. Everything that happens inside stays private.

From the user's wallet#

If the user already holds the asset in the wallet connected to the client, it's one call and one signature:

TypeScript
await senddy.deposit(toUSDC('100'));

// on chains with more than one asset
await senddy.deposit(amount, { token: HOODS });

There's no approval step and no gas. The signature authorizes exactly this deposit into exactly these private notes, so it can't be replayed or redirected.

From anywhere, with a one-time address#

New users often have money somewhere else: an exchange, another wallet, another app. Give them a one-time address to send to. When funds arrive, your server deposits them into the user's private balance.

The pieces are:

  1. In the browser, create a deposit intent. It holds a fresh one-time key derived for this user. Save it durably before you show the address.
  2. The user sends the asset to that address, from anywhere.
  3. Your server watches the address and, when funds land, calls depositWithEphemeralAccount() with a relay submitter, which shields them.
TypeScript
import { createEphemeralDepositIntent, deriveEphemeralDepositAccount } from '@senddy/client';

Build a web app covers the server route this needs and the check it must keep.

From Hyperliquid#

Users can fund their private balance straight from Hyperliquid. Ask the client for an inbound address, save its intent, and show the address:

TypeScript
const inbound = await senddy.hyperliquidInboundAddress({
  saveIntent: async (intent) => db.saveDepositIntent(user.id, intent),
});

inbound.depositAddress; // the user sends here from Hyperliquid

Funds are bridged and land in the user's private balance. Keep the intent and keep watching while the address is shown in your app, because every arrival lands at it.

From other chains#

Deposits from Solana and from other EVM chains work the same way as Hyperliquid: a reusable address on that chain, bridged through Relay into the user's private balance.

TypeScript
import { EVM_BRIDGE_CHAINS } from '@senddy/client';

// Solana: USDC
const sol = await senddy.solanaInboundAddress({
  saveIntent: async (intent) => db.saveDepositIntent(user.id, intent),
});

// Ethereum, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche: USDC. Robinhood Chain: USDG.
const arb = await senddy.chainInboundAddress({
  chainId: EVM_BRIDGE_CHAINS.arbitrum,
  saveIntent: async (intent) => db.saveDepositIntent(user.id, intent),
});

Show a minimum amount: small transfers can cost more to bridge than they're worth. Lighter can't send to a deposit address, so a user coming from Lighter withdraws to their own wallet first.

Withdraw to any address#

TypeScript
await senddy.withdraw('0xRecipient…', toUSDC('100'));

Some assets carry a withdrawal fee, set by the asset's listing or by your app. Read it with getExitFeeBps(token) and show it before the user deposits. See Set your fee.

Withdraw to Hyperliquid#

TypeScript
const out = await senddy.bridgeToHyperliquid({
  recipient: '0xTheirHyperliquidAccount…',
  amount: toUSDC('250'),
  saveRefundIntent: async (intent) => db.saveRefundIntent(user.id, intent),
});

const status = await senddy.bridgeStatus({ txHash: out.txHash });

If the bridge can't fill, the money comes back to a one-time refund address. That's why the refund intent is saved first, before anything moves. The minimum is 5 USDC.

Withdraw to other chains#

TypeScript
// Any EVM chain in EVM_BRIDGE_CHAINS, arriving as that chain's dollar token
await senddy.bridgeToChain({
  chainId: EVM_BRIDGE_CHAINS.ethereum,
  recipient: '0xRecipient…',
  amount: toUSDC('100'),
  saveRefundIntent: async (intent) => db.saveRefundIntent(user.id, intent),
});

// Solana
await senddy.bridgeToSolana({ recipient: 'SolanaAddress…', amount: toUSDC('100'), saveRefundIntent });

// Lighter: the wallet that owns the account, or its account index
await senddy.bridgeToLighter({ recipient: '0xTheirWallet…', amount: toUSDC('100'), saveRefundIntent });

Minimums: 1 USDC to an EVM chain or Solana, 5 USDC to Hyperliquid or Lighter. A Lighter withdrawal is refused before anything moves if the wallet has no Lighter account.

Screening#

Every deposit is checked against sanctions oracles before it's accepted. A flagged address can't deposit. See Compliance.