npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lienfi/sdk

v0.1.0

Published

Price, screen and buy LienFi tax lien certificates from your own code — the calculator, the net-yield scorer, the quote client and the onchain call builders.

Readme

@lienfi/sdk

Price, screen and buy LienFi tax lien certificates from your own code — for vault builders, keepers, analysts and agents.

A read-and-buy client for the LienFi marketplace: the public REST surface, the signed-quote purchase flow, and the addresses, ABIs and EIP-712 definitions needed to send a purchase. It is the same purchase flow the LienFi marketplace itself runs, so you get identical behaviour instead of a re-implementation that drifts.

Valuations are not computed here — the API returns LienFi's own, on every row. See "Every yield the API publishes is GROSS" below.

npm i @lienfi/sdk
npm i viem      # only if you need the onchain helpers

ESM only. Node ≥ 20. viem is an optional peer dependency: the whole REST client runs without it, and even the call builders return plain descriptors — nothing in this package imports viem at runtime.

Licence: PolyForm Shield 1.0.0 — source-available, not open source, and it does not expire. Any purpose is permitted except providing a product that competes with LienFi, so running a keeper, vault, fund, analytics product or agent against LienFi is fully licensed — including modifying this package and redistributing it — while building a rival lien marketplace or valuation product is not. If you redistribute any part of it, carry forward the Required Notice: and Licensor Line of Business: lines. See LICENSE.


Three things to read before you write a buy loop

1. Every yield the API publishes is GROSS

When a lien redeems, LienVault._redeemLien takes interestFeeBps — 10% today — of your gain over what you paid. You keep 90% of the gain.

costBasis      = what you paid
interestEarned = max(redemptiveValueAtRedemption - costBasis, 0)
platformFee    = interestEarned * interestFeeBps / 10_000
youReceive     = redemptiveValueAtRedemption - platformFee

That rate is read at redemption, not snapshotted at purchase. A fee change hits open positions, so a net figure is today's rate applied to a fee levied years from now. Say so when you display one.

Valuations come off the API. LienFi computes them server-side and returns them in each row's calculated block, so you are reading our own numbers rather than reproducing them:

import { LienFiClient } from '@lienfi/sdk';

const client = new LienFiClient();
const { liens, feeConfig } = await client.listLiens({ limit: 100, states: 'FL,TX' });

// null means the API could not read FeeManager. It does NOT mean zero — score
// gross and label it, rather than ranking every lien too high.
if (!feeConfig) throw new Error('no fee rate available; do not rank on net');

const lien = liens[0]!;
const basis = lien.calculated!.current_listing_price!;            // live, recomputed
const projection = lien.calculated!.redemptive_value_projection!;
const atMaturity = projection[projection.length - 1]!.value;
const days = lien.calculated!.days_to_maturity!;

const gain = Math.max(atMaturity - basis, 0);
const netGain = gain * (1 - feeConfig.interest_fee_bps / 10_000);
const netApy = (netGain / basis) * (365 / days) * 100;

Four rows to refuse rather than rank. Each produces a confident number from data that does not exist, and three of them sort to the top of a naive ranking:

| condition | why | | --- | --- | | calculated.degraded | the calculator did not run; apy: 0 and days_to_maturity: 0 are placeholders indistinguishable from a real 0% / matures-today | | redemption_lapsed | visible in the list, but getBuyPrice refuses every one — a top pick here is a quote that 400s | | days_to_maturity <= 0 | dividing by it yields Infinity, and Infinity sorts first | | no projection, or current_listing_price <= 0 | no maturity value, or a zero denominator |

Count what you drop rather than filtering silently — a shortlist that quietly shrank reads as "that is all there was". examples/keeper.ts in the LienFi monorepo implements exactly this, refusals included.

Gross vs net, on a real Florida row. Redemptive value 1895.60 today → 1983.77 at maturity, 302 days out, listed at par:

| figure | value | what it is | | --- | --- | --- | | gross | 5.62% | RV growth over the REMAINING term, before the fee | | net | 5.06% | the same, after 10% of the gain. Rank on this. | | calculated.dynamic_blended_apy | 5.81% | the API's own figure |

The fee costs 0.56pp. ⚠️ dynamic_blended_apy is a genuinely different derivation — interest over the full term from issue, excluding fees and subsequent taxes — so presenting "5.81 → 5.06" as the cost of the fee blames it for 0.75pp it does not cost. Show all three, labelled, rather than picking a misleading pair. Texas makes this unavoidable: its dynamic_blended_apy is hardcoded 0.

Why the calculator is not in this package. An earlier version exported RedemptiveValueCalculator, STATE_CONFIGS and a scoreLien / buildShortlist pair. They are LienFi's valuation model, this package is public, and a bundle cannot hide what it ships. Nothing is lost: the API already computes every figure they produced, and CalculatedLienFinancials documents each field — including which are placeholders on a degraded row.

2. A quote is valid for 300 seconds, is bound to one buyer, and a relist voids it

GET /liens/:id/buy-price?buyer= returns an EIP-712 signature over the price. Four properties, all enforced on chain:

  • 300 seconds. Past deadline, _verifyPriceSignature reverts SignatureExpired.
  • buyer == msg.sender. The signature is verified against the sender, so a quote fetched for one address and sent from another reverts.
  • It embeds the token's current listingNonce. Listing increments it, so a relist between your quote and your send silently invalidates the signature. Read listingNonce (in marketplaceAbi) if you hold a quote for any length of time.
  • maturity must equal the NFT's onchain Maturity, which is written once at mint and has no setter. The API signs the stored redemption deadline for exactly this reason. Never recompute or "freshen" it — _buyNFT compares for equality and reverts "Maturity mismatch".

Fetch a quote immediately before sending. Do not quote a basket and then execute it; the clock is already running while your approve mines.

3. Only part of a quote is authenticated — approve from the signed part

The marketplace's PRICE_TYPEHASH covers exactly eight fields:

tokenId, lienPrice, maturity, deadline, baseValue, feeVersion, buyer, nonce

totalAmount, sellerSurcharge and platformFee are not among them, and nothing on chain ever reads them. They are display values: anyone able to alter the response can set them to anything without invalidating the signature. The test is the typehash, not where the API happens to compute them — they sat below the signing call when this was written and sit above it today, which changes nothing.

So the approve is built from lienPrice:

const calls = buildPurchaseCalls(quote, { chainId: 8453 });

calls.approvalAmount;      // 1895.60 — lienPrice, and what you approve
calls.amountActuallySpent; // 1895.60 — identical, by design
calls.quotedTotalAmount;   // 1945.60 — the quote's own total, for reconciliation only

_collectFeesAndPayout moves exactly lienPrice (platform fee + capital-gains fee + seller proceeds), so this is provably sufficient. The difference is the deprecated seller surcharge — storage slot __deprecated_sellerSurcharge, written 0 on every listing, never transferred.

Approving the larger totalAmount would leave a surplus allowance behind any buyNFT that reverts, and the amount would come from an unauthenticated field. The LienFi marketplace frontend does approve totalAmount, but it sends the pair as one atomic sendCalls userOperation specifically so a reverted buy cannot strand an allowance. Two separate transactions have no such protection.

Set a ceiling. maxApprovalAmount is the only thing that bounds the approve, and the reason is worth being exact about: lienPrice being inside the signature bounds what can be spent, not what can be approved. This package does not verify the quote's signature, so a tampered response can name any lienPrice and have that amount approved — buyNFT then reverts InvalidSignature, and the surplus allowance outlives it. That allowance is dormant rather than drainable (the marketplace's only paymentToken pulls are in _collectFeesAndPayout, whose sole caller passes msg.sender as the buyer), but do not rely on that:

buildPurchaseCalls(quote, { chainId: 8453 }, { maxApprovalAmount: 2_000_000_000n });

If you want the stronger check, the EIP-712 pieces are exported: recover the quote's signer from buyPriceTypes + buildBuyPriceDomain (with the buyer you requested and listingNonce(tokenId)) and compare it against the marketplace's priceSigner.

Both builders throw RangeError before constructing anything if a quote's totalAmount does not equal lienPrice + sellerSurcharge. That is a tamper and corruption signal, not a bound — sellerSurcharge is unauthenticated too, so anyone rewriting one can rewrite the other to match. Treat it as an untrustworthy response rather than retrying it.


Buying

Two transactions, in order. There is no permit path on this marketplace.

A wallet that has never accepted the legal documents cannot get a signed quote at all — getBuyPrice answers 403 consent_required, and this package has no route to clear it. Read "What getBuyPrice cannot yet do for you" below before you wire this up; that is a prerequisite, not an error to handle.

import {
  LienFiClient,
  buildPurchaseCalls,
  quoteSecondsRemaining,
  simulatePurchase,
} from '@lienfi/sdk';

const quote = await client.getBuyPrice(lienId, account.address);
const calls = buildPurchaseCalls(quote, { chainId: 8453 });

// Optional pre-flight. The buy leg reports skipped: 'allowance' until the
// approve has mined — buyNFT pulls USDC, so simulating it earlier always fails
// on the allowance, and that failure says nothing about your quote.
const check = await simulatePurchase({
  publicClient, account: account.address, quote, chainId: 8453,
});

await wallet.writeContract({ ...calls.approve, account, chain });
// …wait for the receipt…
if (quoteSecondsRemaining(quote) <= 0) return reQuote(); // cheaper than reverting
await wallet.writeContract({ ...calls.buy, account, chain });

The builders return plain { address, abi, functionName, args } descriptors — the shape writeContract, simulateContract, encodeFunctionData and any 4337 batcher already take. Nothing in this package imports viem at runtime.

Baskets

buildBulkPurchaseCalls(quotes, { chainId }) gives one approve for the sum of every lienPrice — the signed amounts, for the reason in §3 — and one bulkBuyNFT. The sum of the quotes' own totalAmounts comes back as quotedTotalAmount, for reconciliation only. Max 50 orders (BatchTooLarge() above that — refused client-side with the limit named).

It matters more here than in the single case: bulkBuyNFT skips stale orders and spends less than the sum, so any surplus in the approved amount outlives the transaction by construction.

⚠️ bulkBuyNFT succeeds partially. It skips any order whose listing is no longer active or whose feeVersion is stale, and returns how many it bought. A mined receipt can mean three of your five settled, with no event and no error for the other two. Check ownership afterwards. Orders that fail for any other reason (bad signature, expired deadline, maturity mismatch) revert the whole transaction instead.


Holding liens from a contract

If a vault, a multisig or any contract is going to hold LienFi certificates, four facts decide the design:

  1. There are no transfer restrictions. LienNFT is a plain ERC721URIStorageUpgradeable — no allowlist, no soulbinding, no transfer hook. Any address can hold one.

  2. Delivery uses transferFrom, not safeTransferFrom. Both the vault unlock on purchase (lienVault.unlockNFT → lienNFT.transferFrom) and the redemption pull use the unsafe variant, so your contract does not need onERC721Received. Implementing IERC721Receiver is harmless; omitting it will not block delivery.

  3. Redemption proceeds REQUIRE a one-time setApprovalForAll. The holder must approve the LienVault for the whole collection or redeemLien reverts VaultNotApproved:

    import { buildVaultApprovalCall } from '@lienfi/sdk';
    await wallet.writeContract({ ...buildVaultApprovalCall({ chainId: 8453 }), account, chain });

    Collection-wide, not per-token: ERC-721 clears a token-level approval inside _update on every transfer, so a per-token grant evaporates the moment the lien moves. Idempotent, so calling it twice is free. Your contract must be able to make this call — expose it, or make it part of the deposit path.

  4. Redemption is operator-triggered, not self-serve. LienFi calls redeemLien when the county pays; there is no function your contract calls to redeem on demand. So there is no moment at which a missing vault approval can be noticed and fixed by the holder — it has to be in place beforehand. Grant it right after the first lien arrives.


The REST client

LienFiClient covers the public, unauthenticated read surface and nothing else. fetch only, no runtime dependencies.

| method | route | | --- | --- | | listLiens(params?, signal?) | GET /liens with the full filter set | | getFacets(signal?) | GET /liens/facets — distinct values and real bounds | | getLien(id, signal?) | GET /liens/:id | | getBuyPrice(id, buyer, signal?) | GET /liens/:id/buy-price?buyer= |

new LienFiClient({
  baseUrl: 'https://…/api/v1', // defaults to DEFAULT_API_BASE_URL; include /api/v1
  fetch: myFetchWithRetries,   // defaults to global fetch
  headers: { 'user-agent': 'my-keeper/1.0' },
  timeoutMs: 10_000,
});

Failures throw LienFiApiError with status and path. The message is the API's own sentence, verbatim — a lapsed lien answers Redemption window closed on 2026-07-31. This lien is no longer for sale., which tells you the retry can never succeed. status: 0 means the request never got an answer (DNS, TLS, an aborted signal) and may or may not have reached the API.

Authenticated routes — agent registration, purchase consent, portfolio, relisting — are deliberately absent: they need a Privy session or an operator signature, and a half-implemented auth flow is worse than none. The EIP-712 builders for the operator-authorization documents are exported (buildAgentAuthorizationTypedData and friends), so you can produce a signature the API accepts without copying the type definitions.

What getBuyPrice cannot yet do for you

Signing a quote is the one point where the API refuses a purchase, so it is where every gate lives. Four of them can answer this call, and this client does not carry the two inputs that clear the first two:

  • 403 consent_required — the buyer wallet must hold acceptances of the current legal documents. There is no SDK route to record them: for a human wallet it happens in the app; for an automated one it happens through register_agent on the hosted MCP server at POST /api/v1/mcp, which takes the operator-authorization signature these EIP-712 builders produce. Clear this before you build a buy loop — it is not a transient error, and it refuses every wallet that has never been through one of those two flows.
  • 401 authorization_required — a wallet already bound to an agent authorization may only be quoted by the holder of that authorization blob, presented as Authorization: Bearer <blob> (the same header the MCP endpoint takes), so that nobody else can occupy that wallet's one in-flight quote. LienFiClient sends no such header, so a bound wallet cannot be quoted through it. An unbound keeper wallet is unaffected.
  • 451 sanctioned_address — the buyer is on OFAC's SDN list per Chainalysis' on-chain oracle, or the request came from a blocked jurisdiction. Terminal.
  • 503 sanctions_screening_unavailable or spend_ledger_unavailable — a gate could not be read. Both fail CLOSED by design, so this is the one refusal here that is worth retrying.

The route also takes ?intent=indicative|preflight|signed, which this client does not expose: it always asks for a signed quote. indicative prices a lien without signing, reserving the wallet's quote slot or running the gates above; preflight runs the gates and still signs nothing. Until the client exposes them, reach them with your own fetch — listLiens' calculated block is enough for ranking, and this call is only needed to actually buy.

max_price filters the stored listing_price column, while a quote recomputes the price from live redemptive value — so a lien inside your budget can still quote above it. Carry your own ceiling.


Also exported

  • BASE_BUILDER_CODE / BASE_BUILDER_DATA_SUFFIX — the ERC-8021 Base Builder Code attribution. The call builders attach it as dataSuffix automatically, and viem reads that field in both writeContract and simulateContract, so spreading a descriptor carries it with no extra work. These are exported for the case the builders cannot cover — a wallet client's own dataSuffix, or a 4337 bundler concatenating onto userOp.callData. Pass { builderCode: false } to any builder to opt out; the suffix is trailing bytes no contract in this system reads, so nothing else changes.

  • formatCurrency, formatAddress — display formatting, nothing more.

  • isRedemptionLapsed, isSaleBlockedByLapse, isRedeemableDeed and the two policy booleans. A lapsed redeemable deed stays visible and is not purchasable — what a buyer would be acquiring is the property itself, which the platform can neither convey nor price.

  • addresses / getAddresses / CHAIN_IDS — Base mainnet (8453) and Base Sepolia (84532). lfi is absent on testnet on purpose: eth_getCode at that address on Sepolia returns 0x, so a balanceOf there does not return zero, it fails to decode. Branch on undefined.

  • buyPriceTypes / buildBuyPriceDomain — the EIP-712 definition, so you can recover a quote's signer locally and compare it against the marketplace's priceSigner before spending on it.

  • marketplaceAbi / lienNftAbiMinimal / usdcAbi — narrowed from the generated ABIs (not copied), covering buyNFT, bulkBuyNFT, listingNonce, setApprovalForAll, isApprovedForAll, lienDetails, ownerOf, approve, allowance, balanceOf, decimals. The operator and fee-admin surface is withheld.


Example

A full worked keeper — screen → rank on net yield → quote → pre-flight → approve → buy → vault-approve, with the validity window handled — lives at examples/keeper.ts in the LienFi monorepo. It is env-gated and writes nothing without LIENFI_EXECUTE=1. It is not part of this tarball, because it imports the package from source; every step it takes is shown above.

Risk

Buying a lien spends real USDC on a public blockchain and there is no undo. This SDK has no spend cap: your wallet balance is the mandate. Every yield here is an estimate under today's fee rate, redemption timing is not guaranteed, and nothing in this package is investment, tax or legal advice.

License

PolyForm Shield 1.0.0 — see LICENSE, which carries the two notice lines a redistributor must preserve. Source-available, not OSI open source: if your procurement process requires an OSI-approved licence on production dependencies, talk to us rather than vendoring around it.