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.
- 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.
- 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. 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 viemQuick 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 readEach 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.profitablequoteBuy 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 }); // unlimitedbuy 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.
| Code | When |
|---|---|
INVALID_BTC_ADDRESS | BTC address empty, malformed, or wrong network |
AUCTION_NOT_FOUND | Position was never auctioned |
AUCTION_INACTIVE | Auction is not active |
AUCTION_ALREADY_WON | Someone else cleared it first |
INSUFFICIENT_USDC_BALANCE | Balance below the current price |
ALLOWANCE_REQUIRED | USDC allowance below the current price |
INVALID_AMOUNT | Non-positive approval amount |
POSITION_CONTRACT_UNRESOLVED | Could not resolve the position contract for discovery |
WALLET_REQUIRED, ACCOUNT_UNRESOLVED, WRONG_CHAIN | Signer / 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

