Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Auction SDK

Bid on liquidated BTC collateral with a few lines of TypeScript.

@surgecredit/auction-sdk surfaces the Surge positions up for auction on Base, and the calls to buy their collateral. It is non-custodial and signer-agnostic. You bring viem clients, the SDK builds the calls, the user signs.

Buying is permissionless by protocol design: anyone may call buy to clear an open auction. The flow is listAuctions to find them, quoteBuy to check whether one is worth taking, buy to clear it.

Working in another language or want zero dependencies? The same flow is available as direct contract calls in Smart Contracts.

How a liquidation auction works

Every position is a BTC-collateralized, USDC-denominated credit line identified by a position id.

  1. When a position's collateral ratio falls below its market threshold, a Dutch auction opens against it. The collateral for sale is fixed at open time, so a lower clearing price buys the same BTC for less USDC.
  2. The price steps down at a fixed interval until it reaches a floor, a percentage of the original debt. Always read getCurrentPrice(positionId) rather than assuming the curve.
  3. buy(positionId, btcAddress) pays the current price in USDC, retires the debt, and records your BTC address as the collateral recipient.

Install

npm install @surgecredit/auction-sdk viem

Quick start

import { createWalletClient, custom } from "viem";
import { base } from "viem/chains";
import { SurgeAuctionClient, createSurgeAuctionPublicClient } from "@surgecredit/auction-sdk";
 
const publicClient = createSurgeAuctionPublicClient("https://mainnet.base.org", base);
const walletClient = createWalletClient({ chain: base, transport: custom(window.ethereum) });
 
const auctions = new SurgeAuctionClient({
  publicClient,
  walletClient,       // omit for a read-only client
  network: "mainnet", // "signet" = Base Sepolia
});
 
for (const a of await auctions.listAuctions()) {
  console.log(a.positionId, a.currentPriceUsdc, a.collateralBtc);
}

Reads

await auctions.listAuctions();            // AuctionSnapshot[], buyable only by default
await auctions.getAuction(positionId);         // AuctionInfo | null
await auctions.getCurrentPrice(positionId);    // { raw, usdc }
await auctions.getOracleBtcPriceUsd();    // BTC/USD from the protocol oracle
await auctions.quoteBuy(positionId, opts);     // BuyQuote: profitability at current price
await auctions.getUsdcBalance(owner);
await auctions.getAllowance(owner);       // raw USDC allowance to the AuctionHouse
await auctions.getAuctionHouseAddress();  // resolved from the VaultManager
await auctions.getPositionContractAddress();

Listing auctions

listAuctions returns auctions with their current price, batched into a single round trip.

await auctions.listAuctions();                            // buyable: active and unwon
await auctions.listAuctions({ onlyBuyable: false });      // include cleared and settled
await auctions.listAuctions({ positionIds: [396, 415] }); // known ids, skips discovery
await auctions.listAuctions({ includeMarketId: false });  // one less batched read

Each entry is an AuctionSnapshot:

{
  positionId: string;
  status: "open" | "purchased" | "settled";
  currentPriceUsdc: number;
  currentPriceRaw: bigint;
  debtToPay: number;
  collateralBtc: number;
  collateralSats: bigint;
  token: Address;
  originalOwner: Address;
  createdAt: number;
  expiresAt: number;
  expired: boolean;
  active: boolean;
  won: boolean;
  winnerEvm: Address;
  winnerBtcAddress: string | null;
  marketId: string | null;
}

Legacy positions report marketId as the maxUint256 sentinel. Use the exported isLegacyMarket helper to detect it.

Quote profitability

quoteBuy values the collateral at the oracle BTC price and applies your margin gate. With no options, profitable means the price is below collateral value. It is false for any auction you cannot actually buy, whatever the numbers say.

const q = await auctions.quoteBuy(positionId, {
  estBtcDeliveryFeeUsd: 3, // assumed BTC miner fee per delivery
  targetMarginBps: 300,    // require 3% of collateral value as net profit
});
// q.collateralValueUsd, q.priceUsdc, q.netProfitUsd, q.requiredProfitUsd, q.profitable

quoteBuy is for one auction you are considering. It reads the auction, its price and the oracle on every call, so calling it in a loop refetches the same oracle price once per auction. To score a whole list, read the oracle once and reuse it with the exported computeBuyEconomics helper, which is pure and needs no network:

import { computeBuyEconomics } from "@surgecredit/auction-sdk";
 
const btcPriceUsd = await auctions.getOracleBtcPriceUsd();
 
const scored = (await auctions.listAuctions())
  .filter((a) => a.currentPriceUsdc > 0) // skip auctions with no readable price
  .map((a) => ({
    positionId: a.positionId,
    ...computeBuyEconomics({
      priceUsdc: a.currentPriceUsdc,
      collateralSats: a.collateralSats,
      btcPriceUsd,
      estBtcDeliveryFeeUsd: 3,
      targetMarginBps: 300,
    }),
  }))
  .filter((a) => a.profitable);

Buy

const address = "0xBuyerWallet";
 
// Approve what this auction needs, plus 5% headroom. No-ops if already covered.
await auctions.approveForBuy({ positionId, account: address, waitForReceipt: true });
 
// Buy at the current price; seized BTC is sent to your Bitcoin address.
await auctions.buy({ positionId, btcAddress: "bc1q...", account: address, waitForReceipt: true });

approveForBuy approves the current price plus bufferBps of slack, 500 (5%) by default, and returns null without sending anything if your allowance already covers it. approveUsdc is there for an exact or unlimited allowance instead:

await auctions.approveUsdc({ account: address, amountUsdc: "50" }); // exact
await auctions.approveUsdc({ account: address });                   // unlimited

buy pre-flights the auction state, your USDC balance and allowance, and validates the BTC address for the network, throwing a typed SurgeAuctionError instead of reverting. A wrong-network BTC address is rejected up front, since seized BTC sent to it is unspendable. The address is trimmed before both validation and encoding, so the two cannot disagree.

Base RPCs sometimes reject the gas estimate for buy with "exceeds max transaction gas limit". Pass an explicit limit to bypass the estimate:

await auctions.buy({ positionId, btcAddress: "bc1q...", gasLimit: 5_000_000n });

Errors

Every error the SDK raises is a SurgeAuctionError with a stable code, so you branch on the code instead of matching message strings.

import { SurgeAuctionError } from "@surgecredit/auction-sdk";
 
try {
  await auctions.buy({ positionId, btcAddress });
} catch (err) {
  if (err instanceof SurgeAuctionError && err.code === "AUCTION_ALREADY_WON") {
    // someone cleared it first
  }
  throw err;
}

Errors from viem pass through unchanged, so an RPC failure or a rejected signature is not a SurgeAuctionError.

CodeWhen
INVALID_BTC_ADDRESSBTC address empty, malformed, or wrong network
AUCTION_NOT_FOUNDPosition was never auctioned
AUCTION_INACTIVEAuction is not active
AUCTION_ALREADY_WONSomeone else cleared it first
INSUFFICIENT_USDC_BALANCEBalance below the current price
ALLOWANCE_REQUIREDUSDC allowance below the current price
INVALID_AMOUNTNon-positive approval amount
POSITION_CONTRACT_UNRESOLVEDCould not resolve the position contract for discovery
WALLET_REQUIRED, ACCOUNT_UNRESOLVED, WRONG_CHAINSigner / chain setup

Next steps

That is the whole surface: list auctions, quote one, buy it. The SDK holds no keys and takes no custody, so the wallet you pass in stays yours throughout.

Working in another language, or want zero dependencies? The same flow is available as direct contract calls in Smart Contracts.

v0.1.1