Skip to main content
In headless mode you keep full control of the UI and wallet connection. Barker provides:
  • REST API for product metadata, APY history, fee stats, user position, vault health
  • Standard ERC-4626 contract (BarkerEngine) for deposit / withdraw — call it directly from your frontend with any wallet library
  • Webhooks for APY changes, TVL alerts, vault pauses
If you’d rather drop in a ready-made UI, see Embed instead.

End-to-end flow

1. List products and pick one

const res = await fetch("https://api.barker.money/api/partner/products", {
  headers: { "X-Api-Key": process.env.BARKER_API_KEY! },
});
const { data: products } = await res.json();

const product = products.find((p) => p.slug === "acme-usdc-base");
// product.engine_contract_address  → call deposit/redeem here
// product.asset_address             → USDC contract for approve()
// product.asset_decimals            → 6
// product.chain_id                  → switch chain check
// product.latest_metrics.net_apy_for_user → display APY
// product.risk_label                → "conservative" | "balanced" | "aggressive" | null (optional, for multi-tier UIs)
Cache this response for ~60 seconds — products change rarely.

2. Connect the user’s wallet

Use whatever you already have — Wagmi / RainbowKit / Privy / your own connector. Barker doesn’t care. The only requirement: by the time the user clicks “Deposit”, their wallet is connected to the same chain_id as the product (e.g. Base = 8453).
import { useAccount, useSwitchChain } from "wagmi";

const { address, chainId } = useAccount();
const { switchChain } = useSwitchChain();

if (chainId !== product.chain_id) {
  await switchChain({ chainId: product.chain_id });
}

3. Deposit

BarkerEngine is a standard ERC-4626 vault. Two transactions: approve the underlying token, then deposit.
import { parseUnits } from "viem";
import { erc20Abi } from "viem";
import { writeContract, waitForTransactionReceipt } from "@wagmi/core";
import { engineAbi } from "./abi/BarkerEngine"; // see Smart Contracts page

const amount = parseUnits("10000", product.asset_decimals); // 10,000 USDC

// 1. approve
const approveTx = await writeContract(config, {
  address: product.asset_address,
  abi: erc20Abi,
  functionName: "approve",
  args: [product.engine_contract_address, amount],
});
await waitForTransactionReceipt(config, { hash: approveTx });

// 2. deposit
const depositTx = await writeContract(config, {
  address: product.engine_contract_address,
  abi: engineAbi,
  functionName: "deposit",
  args: [amount, address], // receiver = the same user
});
await waitForTransactionReceipt(config, { hash: depositTx });
After the deposit confirms, refresh the user’s position (next step).
Tip — single-transaction UX: the underlying USDC contract on most chains supports EIP-2612 permit. If you collect a permit signature off-chain you can replace the approve tx with a no-cost signature, then bundle permit + deposit in a single transaction. See Smart Contracts → permit flow.

4. Show the user’s position

const res = await fetch(
  `https://api.barker.money/api/partner/products/${product.slug}/position?address=${address}`,
  { headers: { "X-Api-Key": process.env.BARKER_API_KEY! } },
);
const { data: position } = await res.json();
// position.shares     → engine shares (string, 18 decimals)
// position.assets     → underlying assets (string, asset_decimals)
// position.usd_value  → display string, e.g. "10500.12"
For real-time updates, also read engine.balanceOf(user) directly from the chain — the API serves the same value but with ~30s edge cache.

5. Withdraw / redeem

Two ERC-4626 entrypoints:
  • engine.redeem(shares, receiver, owner) — burn N shares, receive whatever assets they’re worth
  • engine.withdraw(assets, receiver, owner) — receive exactly N assets, burn whatever shares are required
Most UIs show “Withdraw X"useredeemafterconvertingX" → use `redeem` after converting `X → sharesviaengine.previewWithdraw(assets)`.
viem
const sharesToBurn = await readContract(config, {
  address: product.engine_contract_address,
  abi: engineAbi,
  functionName: "previewWithdraw",
  args: [parseUnits("5000", product.asset_decimals)], // user wants $5,000 back
});

const tx = await writeContract(config, {
  address: product.engine_contract_address,
  abi: engineAbi,
  functionName: "redeem",
  args: [sharesToBurn, address, address],
});
await waitForTransactionReceipt(config, { hash: tx });
redeem is one transaction — no approve needed (engine shares are owned by the user already).

6. Listen for on-chain events (optional)

If you want server-side reconciliation rather than polling, subscribe to engine events:
EventWhenUseful for
Deposit(sender, owner, assets, shares)User depositsConfirm deposit landed, update internal ledger
Withdraw(sender, receiver, owner, assets, shares)User redeemsConfirm withdrawal landed
FeeAccrued(barkerFeeShares, partnerFeeShares, totalAssets, ...)Yield is splitReconcile partner fee earnings
ABI fragments and full event signatures: Smart Contracts → events.

7. Subscribe to webhooks (optional)

For events Barker pushes — lifecycle (deposit.confirmed, redeem.confirmed, redeem.requested) and operational (apy_change, tvl_alert, vault_pause) — see Webhooks and Deposit/Redeem Lifecycle. Not strictly required for a working UI (you can watch chain logs from the client — see the reference implementation below), but strongly recommended for backend reconciliation: DB writes, accounting, ops alerts.

Reference implementation — Next.js + wagmi

The snippets above show each step in isolation. Here’s the same flow as a single Next.js 16 App Router page that you can copy and adapt — three files, runnable end to end. Architecture in one sentence: server component fetches product metadata with the API key (which never leaves the server); client component drives wallet + writes + uses wagmi’s useWatchContractEvent for instant UI updates; a separate Route Handler receives Barker webhooks for backend reconciliation (DB, accounting). The two channels are independent — your UI doesn’t block on webhook delivery, and your backend doesn’t depend on the user keeping the tab open.
app/
  yield/[slug]/
    page.tsx                ← server component, fetches product
    yield-card.client.tsx   ← 'use client', wagmi tx + chain log watcher
  api/webhooks/barker/
    route.ts                ← HMAC verify, dedupe, hand off to your DB
// Server component — API key stays on the server.
import { YieldCard } from "./yield-card.client";

type Params = { slug: string };

export default async function YieldPage(
  { params }: { params: Promise<Params> },
) {
  const { slug } = await params;
  const res = await fetch(
    `https://api.barker.money/api/partner/products/${slug}`,
    {
      headers: { "X-Api-Key": process.env.BARKER_API_KEY! },
      next: { revalidate: 60 }, // cache product metadata for 60s
    },
  );
  if (!res.ok) throw new Error(`product fetch failed: ${res.status}`);
  const { data: product } = await res.json();
  return <YieldCard product={product} />;
}
Two channels, two purposes — keep them straight:
ChannelLives inTriggersPurpose
useWatchContractEventBrowserEngine Deposit / Withdraw log seen by user’s RPCInstant UI refresh while the user is on the page
Webhook Route HandlerYour backendBarker watcher confirms log at depth, signs + deliversDurable backend state — DB writes, accounting, ops alerts
If the user closes the tab mid-deposit, the UI watcher misses the event but the webhook still fires — your backend stays consistent. If your backend webhook endpoint is down, the UI still updates instantly — Barker retries the webhook for you. Don’t try to make either channel do both jobs. What’s omitted from the snippet (kept tight on purpose; add as you go):

Sandbox checklist

Before you commit production engineering time, verify the headless flow against sandbox:
  • bk_test_* key issued (Portal → API Keys)
  • Sandbox product slug visible in GET /api/partner/products
  • Sandbox USDC obtained from the Sandbox page → Mint Test USDC
  • approve + deposit round-trips on testnet, position endpoint reflects new balance within 30s
  • redeem round-trips, position drops to 0
  • Same code path works against bk_live_* key (only the key changes, base URL is identical)

Production checklist

  • Production engine deployed (status active in Portal → Products)
  • Frontend reads chain_id from the product response and switches chain before signing
  • Error states surfaced: vault paused (poll /health), insufficient allowance, user rejected signature, network mismatch
  • Slippage guard / minimum shares check on deposit (previewDeposit matches your displayed APY ± a tolerance)
  • Compliance disclosures shown to user before deposit

What’s next