Skip to main content
The TypeScript SDK is WIP. This guide shows the raw two-step flow you can wire up today.

Prerequisites

  • A wallet on the source chain with testnet USDC (or any supported asset)
  • tokenIn approval to the source-chain bridge contract (addresses in supported chains)
  • The recipient registered via POST /recipient/register at least once (generates their stealth keys)
  • An ECIES library for the language you’re using

1. Register the recipient (first time only)

const sig = await recipientWallet.signMessage(`REGISTER_STEALTH:${recipientAddress}`);
const res = await fetch(`${RELAYER_URL}/recipient/register`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ recipientAddress, signature: sig }),
});
Calling again for the same address is idempotent and just returns existing keys.

2. Approve the bridge contract

The sender must approve tokenIn to the bridge on the source chain for amountIn + reward:
import { Contract } from "ethers";
const token = new Contract(tokenIn, erc20ABI, senderWallet);
await (await token.approve(bridgeAddress, amountIn + reward)).wait();

3. Call createIntent on source chain

This publishes the public solver-facing offer. The contract also emits an encrypted blob in the same event recoverable only by the relayer’s key, the on-chain recovery anchor.
import { Contract } from "ethers";
const bridge = new Contract(bridgeAddress, bridgeIntentABI, senderWallet);

const tx = await bridge.createIntent(
  tokenIn,               // _tokenA
  tokenOut,              // _tokenB
  amountIn,              // _amountA
  minAmountOut,          // _expectedAmountB, your slippage floor
  reward,                // _reward, solver incentive
  destChainId,           // _destChainId (numeric, e.g., 26514 for Horizen)
  auctionDuration        // _auctionDuration, seconds, minimum 20
);
await tx.wait();

const intentId = (await bridge.getLatestIntentId()).toString();
At this point the intent is live on-chain but the relayer doesn’t yet know who the funds go to on the destination side. That comes next.

4. Fetch the relayer’s ECIES public key

const { publicKey: relayerEciesPubkey } = await fetch(`${RELAYER_URL}/ecies-pubkey`).then(r => r.json());

5. Encrypt the recipient bundle and store it

The bundle contains the actual recipient address(es) and per-recipient amounts on the destination chain. Sum of amounts must be ≤ minAmountOut.
const plaintext = JSON.stringify({
  recipients: ["0xRecipient..."],
  amounts:    ["4950000"],  // base units of tokenOut on destination
});

const encrypted = await eciesEncrypt(relayerEciesPubkey, Buffer.from(plaintext));

await fetch(`${RELAYER_URL}/store-recipients`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    intentId,
    chainId: destChainId,
    encryptedData: encrypted.toString("hex"),
  }),
});

6. Track the intent to completion

Poll GET /intent-details/:intentId until status === "completed":
async function waitForCompletion(intentId: string) {
  while (true) {
    const { intent } = await fetch(`${RELAYER_URL}/intent-details/${intentId}`).then(r => r.json());
    if (intent.status === "pending")  ui.show("Awaiting solver bids");
    if (intent.status === "solving")  ui.show("Solver fulfilling on destination");
    if (intent.status === "settled")  ui.show(`Delivered: ${intent.solveTxHash}`);
    if (intent.status === "completed") {
      ui.show(`Source settled: ${intent.settleTxHash}`);
      return intent;
    }
    await sleep(3000);
  }
}
See tracking intents for the full state machine.

What happens under the hood

After step 5, the relayer:
  1. Batches your intent with other live intents that share the same token pair + chain pair
  2. Broadcasts to solvers in plaintext: rate, token pair, total batch amount, available fees. No sender or recipient info.
  3. When a solver bids to fill some portion of the batch, re-encrypts your recipient bundle to that solver’s keypair
  4. Solver delivers to the stealth address(es) on the destination chain
  5. Relayer settles on source chain (releases the solver’s reward)
The only actor that ever sees the plaintext recipient address (other than the relayer inside its trusted environment) is the single solver that won the bid for your intent.

Recipient side

The recipient’s funds land at a Safe at a fresh stealth address on the destination chain. To sweep:
  1. Recipient registers if they haven’t already (step 1 above)
  2. Lists their stealth balances via GET /recipient/addresses
  3. Derives the stealth key for a specific delivery via POST /recipient/derive-stealth-key
  4. Signs a Safe tx and calls POST /recipient/relay-proxy, the relayer broadcasts and pays gas
See retrieving funds for the full recipient flow.

Error handling

Common failure modes:
WhereWhatHow to fix
createIntent revertInsufficient allowance / balanceApprove enough before calling
createIntent revertInvalid destination chain IDUse a chain from supported chains
POST /store-recipients 400Intent ID doesn’t exist on-chainWait for createIntent tx to mine before calling
POST /store-recipients 400ECIES decryption failureRe-fetch /ecies-pubkey; ensure your library uses the same curve format
Intent stays pending past auctionNo solver took the offerRetry with a higher reward or a more liquid chain pair
Intent expiresauctionDuration elapsed with no winnerUser is refunded via BridgeIntentV2 escape; funds are safe
See error reference for the complete list.

Try it on testnet

Five-minute version of this guide.