Marketplace API

Every listing, its terms and its live economics as JSON — no key, no account, no browser. This is the same data the marketplace itself renders.

Building with an agent? Paste this in.

Everything a coding agent needs to integrate correctly on the first attempt: the base URLs, where to start, and the nine rules that are not guessable from the field names. The rest of this page is the same material for a human.

brief
Integrate the LienFi marketplace API — tokenized US tax lien certificates and
redeemable tax deeds, settled in USDC on Base.

No API key, no account, no Authorization header. Read these first:

  OpenAPI 3.1   https://api.lienfi.com/api/v1/openapi.json
  Conventions   https://app.lienfi.com/llms.txt
  Reference     https://app.lienfi.com/docs/api

BASE URLS
  https://api.lienfi.com/api/v1
      The complete record. Every response is wrapped in
      { success, data, meta }. NOT wildcard-CORS — call it server-side.
  https://api.lienfi.com/api/public
      A curated projection: flat camelCase, no envelope, an explicit field
      allowlist, no wallet addresses. Sends Access-Control-Allow-Origin: *.
  POST https://api.lienfi.com/api/v1/mcp
      Model Context Protocol, JSON-RPC 2.0 over stateless HTTP, no credential.
      Read tools only: market_overview, search_liens, get_lien. Nothing here
      signs, submits or reserves anything.

START WITH
  GET https://api.lienfi.com/api/v1/liens/facets
      Which states and counties actually have inventory, and the real numeric
      bounds. Call this before filtering, or an empty page is ambiguous.
  GET https://api.lienfi.com/api/v1/liens?states=FL&max_price=50000&sort_by=apy_high_low&limit=5
      The listing book. ~30 optional query params. limit caps at 100.
  GET https://api.lienfi.com/api/public/liens/{id}
      One lien, flat, carrying the NET per-year rate. Start here unless you
      need the full record.

RULES THAT ARE NOT GUESSABLE FROM THE FIELD NAMES

1. Every yield on the versioned tree is GROSS of our fee. calculated.apy also
   compounds, and has no consumer in our own interface. The net figure is
   netPerYearPercent on the curated tree, and it is what sort_by=apy_high_low
   ranks on. Do not describe "the yield" without saying which one.

2. The fee is taken from the GAIN over what the buyer paid, floored at zero —
   never from the redemption value. A lien redeeming below its purchase price is
   charged nothing. Read the live rate from meta.fee_config; do not hardcode it.

3. redemptive_value, accrued_interest and listing_price on the raw row are frozen
   snapshots, written at create/update time, and stale on essentially every row.
   The calculated block is recomputed on read — use
   calculated.current_redemptive_value and calculated.current_listing_price.

4. Rates are annualized SIMPLY (return * 365 / days), not compounded: a
   certificate pays once at redemption and there is nothing to reinvest. Under 30
   days to maturity there is no per-year rate at all — expect null, never 0.

5. redemption_deadline is the statutory deadline and is immutable onchain from
   the moment the lien is minted. Never recompute or infer it.

6. An unknown query parameter is DROPPED, not rejected. ?max_prise=50000 answers
   200 with success: true and the entire unfiltered book. Nothing in the response
   reports the filter you meant to send — watch meta.pagination.total move.

7. status=listed also matches relisted; statuses=listed is taken verbatim and
   silently returns fewer rows. Send statuses=listed,relisted for everything on
   offer.

8. A 400 arrives in two different shapes. Schema-level failures (limit over 100,
   a bad enum, a malformed id) carry no success key at all; policy failures use
   the envelope. Branch on the HTTP status code, not on body.success.

9. Price filters and price sorts run in the database against the stored columns
   in rule 3, so max_price and price_low_high can disagree by a few dollars with
   the figure you then read out of calculated. The discount_* and apy_* sorts do
   not have this problem.

A tax lien is not a deposit and not a bond. Redemption is the expected outcome,
not a guaranteed one, and the remedy otherwise is foreclosure on the property.
Read https://app.lienfi.com/legal/risk-disclosure-statement before presenting any of this as an
investment recommendation.

Overview

One marketplace, three ways to read it. Nothing below needs an API key, an account or an Authorization header — with one exception, called out on the route it applies to.

  • REST. Two trees — the complete versioned record, and a curated flat projection for consumers that are not browsers. Start at GET /liens/facets, then GET /liens.
  • OpenAPI 3.1. Built from the routes themselves, so it cannot describe a parameter the API does not accept. Point a generator at it rather than hand-writing a client.
  • MCP. If your client speaks Model Context Protocol, prefer it: its search_liens ranks on net yield using the same arithmetic this site displays, so you get our numbers rather than your reconstruction of them.

The book runs to 250+ live tax lien certificates and redeemable tax deeds. Terms are set by state statute and differ by state, so which states are represented changes with the inventory — ask /liens/facets rather than trusting a list on a page, and read the statutory mechanics for the rules each state sets.

Base URLs

Two trees, on purpose. Which one you want depends on whether you are calling from a browser and whether you need the complete record.

versioned tree
https://api.lienfi.com/api/v1
curated public tree
https://api.lienfi.com/api/public

The versioned one is the complete record and uses a { success, data, meta } envelope like every other REST route here. The /api/public tree is a curated projection for consumers that are not browsers: flat camelCase, no envelope, an explicit field allowlist, and no wallet addresses or transaction history. public there is a promise about the contract, not a version, which is why it does not sit under one.

Quickstart

Three calls that cover most of what anyone builds.

curl
# Texas liens under $50k, best net per-year rate first
curl -s 'https://api.lienfi.com/api/v1/liens?states=TX&max_price=50000&sort_by=apy_high_low&limit=5'

# What is actually in stock, before you filter blind
curl -s 'https://api.lienfi.com/api/v1/liens/facets'

# One lien, flat, no envelope
curl -s 'https://api.lienfi.com/api/public/liens/<id>'

There is nothing to sign up for and nothing to send in a header. If you get a CORS error, you are calling the versioned tree from a browser — see Limits, CORS and caching. The one call that can still refuse you is buy-price, which needs the buyer's wallet to belong to a consenting account.

Reading the numbers

Four things that are not guessable from the field names. Each one has been got wrong before.

Every yield this API returns is gross

calculated.apy and dynamic_blended_apy are both gross of our fee, and apy additionally compounds. Neither has a consumer in our own interface, and neither is the number we show a buyer. The net figure is on https://api.lienfi.com/api/public/liens/{id} as netPerYearPercent, and it is what sort_by=apy_high_low ranks on.

The fee is charged on the gain, not on the value

A buyer pays listing_price and nothing else. At redemption the vault takes interest_fee_bps of the gain over that purchase price — floored at zero, so a lien redeeming below what the buyer paid is charged nothing. At 1000 bps, a $1,000 lien redeeming at $1,100 is charged $10 and the buyer keeps $1,090.

Three columns on the raw row are frozen snapshots

redemptive_value, accrued_interest and listing_price are written at create and update time and are stale on essentially every row — Florida interest accrues monthly, so a row drifts from the day it is written. The calculated block is recomputed on every read. Use calculated.current_redemptive_value and calculated.current_listing_price.

One consequence to plan around: the filters and the price sorts run in the database, on those same stored columns. So max_price, min_ltv and price_low_high can disagree slightly with the figure you then read out of calculated — a lien can sit a few dollars either side of a bound you set. The discount_* and apy_* sorts do not have this problem; those are ranked on the recomputed values.

Per-year rates annualize simply, and stop under 30 days

Every per-year figure we publish is return × 365 / days, not a compounding rate: a certificate pays once at redemption and there is nothing to reinvest. Below 30 days to maturity we publish no per-year rate at all rather than a large one — there is no year there to restate over — and netPerYearPercent is null for those liens, never 0.

One more, if you are pricing rather than screening: redemption_deadline is the statutory deadline and is immutable onchain from the moment a lien is minted. It is not recomputed and must not be inferred from an issue date and a term.

Endpoints

The whole public read surface. The write paths, everything under /portfolio and the admin tree need an authenticated session and are not documented here.

GET/liens

https://api.lienfi.com/api/v1/liens

The listing book: filtered, sorted, paginated.

Every parameter in the Query parameters section applies to this route, and every one of them is optional. The defaults return what is currently for sale, newest first, twenty to a page.

This is the only route that carries the fee rate in meta.fee_config for a whole page of liens at once — read it from there rather than assuming 1000 bps.

Response

An array of full lien records at data, plus pagination, filters and fee config at meta.

example
curl -s 'https://api.lienfi.com/api/v1/liens?states=TX&max_price=50000&sort_by=apy_high_low&limit=5'

GET/liens/facets

https://api.lienfi.com/api/v1/liens/facets

Distinct filter values and the real numeric bounds of live inventory.

Call this before guessing which states or counties have stock. It is the difference between an empty page that means "nothing matched" and one that means "you filtered on a county we have never listed".

Response

The distinct states, counties, municipalities and property types on the live book, plus min/max for price, interest and LTV.

cached max-age 5 min
example
curl -s 'https://api.lienfi.com/api/v1/liens/facets'

GET/liens/{id}

https://api.lienfi.com/api/v1/liens/{id}

One lien in full — every public column, plus the live calculated block.

Parameters

  • id path · uuid · requiredThe lien id. A malformed one is a 400, not a 404.

Response

The complete record, including calculated (recomputed on read) and the two redemptive-value projections.

example
curl -s 'https://api.lienfi.com/api/v1/liens/{id}'

GET/liens/{id}

https://api.lienfi.com/api/public/liens/{id}

The same lien as flat camelCase JSON with no response envelope. Start here.

The investment facts on an explicit allowlist — no wallet addresses, no transaction history, one nesting level. It is also the only REST route that hands you the NET per-year rate rather than leaving you to apply the fee yourself; over MCP, search_liens ranks on the same figure.

netPerYearPercent is null, never 0, for a lien with under 30 days remaining, no cost basis, or a row that cannot be priced.

Parameters

  • id path · uuid · requiredThe lien id.

Response

A flat object, or { error, message } with a 404 / 500. No success key either way.

no envelopeCORS: *cached max-age 5 min
example
curl -s 'https://api.lienfi.com/api/public/liens/{id}'

GET/liens/map

https://api.lienfi.com/api/v1/liens/map

The same filter surface as /liens, unpaginated, as slim location records.

It accepts page and limit and ignores them — deliberately, so that the map and the table can never disagree about which liens matched a filter.

Response

A data.locations ARRAY of slim location records — id, coordinates, price and status. Note the extra nesting: data is an object here, not the array itself, which is the opposite of every other list route on this tree.

example
curl -s 'https://api.lienfi.com/api/v1/liens/map'

GET/liens/{id}/buy-price

https://api.lienfi.com/api/v1/liens/{id}/buy-price

A signed EIP-712 quote for a named buyer address.

Read-only, but it is the purchase path: the signature binds a price and a maturity that the marketplace contract checks onchain. A quote is minted for the address you name and for no other.

THE ONE ROUTE ON THIS PAGE THAT CAN REFUSE YOU. It takes no Authorization header, and it still answers 403 { code: "consent_required" } unless the buyer address is linked to a LienFi account that has accepted the current agreements — an unrecognised wallet fails closed. So the example below returns 403, by design: quoting a price to a wallet we cannot show has agreed to the terms is the thing the gate exists to prevent. Screening and pricing need none of this; only minting a signature does.

The signed maturity is the lien’s stored redemption_deadline, which is immutable onchain from the moment the lien is minted. It is not recomputed, and a quote carrying a "fresher" one would revert.

Parameters

  • id path · uuid · requiredThe lien id.
  • buyer query · address · requiredThe wallet that will submit the purchase, 0x-prefixed.

Response

The typed data, the signature, lienPrice and totalAmount in micro-USDC. Approve totalAmount — it carries the listing fee, which the contract never transfers but the ERC-20 approve must still cover.

example
curl -s 'https://api.lienfi.com/api/v1/liens/{id}/buy-price?buyer=0x0000000000000000000000000000000000000000'

GET/liens/{id}/street-view

https://api.lienfi.com/api/v1/liens/{id}/street-view

A cached Street View image URL for the property, if one could be resolved.

Parameters

  • id path · uuid · requiredThe lien id.

Response

An image URL, or a null-ish payload for a property with no resolvable panorama.

example
curl -s 'https://api.lienfi.com/api/v1/liens/{id}/street-view'

GET/liens/{id}/static-map

https://api.lienfi.com/api/v1/liens/{id}/static-map

A cached aerial map image URL for the property.

Parameters

  • id path · uuid · requiredThe lien id.

Response

An image URL.

example
curl -s 'https://api.lienfi.com/api/v1/liens/{id}/static-map'

GET/market/activity

https://api.lienfi.com/api/v1/market/activity

Recent sale results — what filled, and at what price.

No wallet addresses. This is the endpoint for a historical track record; GET /liens is not, because it filters out expired certificates whatever status you ask for.

Parameters

  • limit query · integerEvents to return, 1–25. Defaults to 10.
  • include_summary query · booleanAdds lifetime sale totals to meta. Defaults to false.

Response

Recent fills with their prices and lots.

cached max-age 30 s
example
curl -s 'https://api.lienfi.com/api/v1/market/activity?limit=10&include_summary=true'

https://api.lienfi.com/api/v1/legal

One legal document.

Six valid types; the ones to read are terms-and-conditions and risk-disclosure-statement.

An unpublished type answers 200 with empty content and version 0 rather than 404 — so check version before rendering, or you will publish a blank document as though it were the terms.

Parameters

  • type query · enum · requiredThe document slug, e.g. terms-and-conditions.

Response

The document body, its version and its effective date.

example
curl -s 'https://api.lienfi.com/api/v1/legal?type=risk-disclosure-statement'

GET/openapi.json

https://api.lienfi.com/api/v1/openapi.json

This API as OpenAPI 3.1, built from the routes themselves.

Point a generator at it rather than hand-writing a client. It is built from the same schema blocks the routes validate against, so it cannot describe a parameter the API does not accept.

Response

An OpenAPI 3.1 document, roughly 30 KB.

CORS: *cached max-age 5 min
example
curl -s 'https://api.lienfi.com/api/v1/openapi.json'

POST/mcp

https://api.lienfi.com/api/v1/mcp

The Model Context Protocol endpoint. JSON-RPC 2.0, no credential.

Covered in full in its own section below. Listed here because it is part of the same public surface and answers on the same base URL.

Response

A JSON-RPC 2.0 response. Not the REST envelope — the client here is an MCP implementation.

no envelopeCORS: *

Query parameters

GET/liens and /liens/map take the same set. Every parameter is optional. Where a plural and a singular form both exist, the plural is comma-separated and wins outright if you send both.

A misspelled parameter is silently ignored

Unknown query parameters are dropped, not rejected. So ?max_prise=50000 answers 200 with success: true and the entire unfiltered book — which reads as a filter that matched everything rather than as a typo. Nothing in the response will tell you either: meta.filters echoes only status, the singular state and county, and the price and interest ranges, so a working property_types is just as absent from it as a misspelled one. Watch meta.pagination.total move instead, and check names against the tables below.

Paging

The book runs to 250+ rows, and this endpoint never returns it in one response.

ParameterMeaning
pageinteger · default 1Page number, 1-based.
limitinteger · default 20Rows per page. Caps at 100 — a larger value is a 400, not a clamp.

Price, value and rate

Bounds are inclusive. A minimum above its own maximum is a 400, not an empty page.

ParameterMeaning
min_pricenumberMinimum asking price, USD.
max_pricenumberMaximum asking price, USD.
min_face_valuenumberMinimum face value — the delinquent tax the certificate was struck for, not what it sells for.
max_face_valuenumberMaximum face value, USD.
min_assessed_valuenumberMinimum county-assessed value of the underlying property, USD.
max_assessed_valuenumberMaximum county-assessed value, USD.
min_interestnumberMinimum statutory interest rate, percent. A Texas redeemable deed carries no interest rate — it has a § 34.21 premium instead — so any lower bound above zero excludes the entire Texas book.
max_interestnumberMaximum statutory interest rate, percent.
min_ltvnumberMinimum loan-to-value, percent. Accepts 0–999.99; that ceiling is the column width, not a business rule.
max_ltvnumberMaximum loan-to-value, percent.

Location

Call /liens/facets first — it returns the states and counties that actually have inventory, so you are not guessing.

ParameterMeaning
statestringOne two-letter state code.
statescsvUp to 10 state codes. Overrides state.
countystringOne county name, matched exactly.
countiescsvUp to 50 county names. Overrides county.
municipalitystringOne municipality name. Null on much of the book — prefer county.
municipalitiescsvUp to 50 municipality names. Overrides municipality.

Property and terms

Enumerated. Anything outside the listed values is a 400.

ParameterMeaning
lien_typeenumCertificate or redeemable deed. The two accrue and mature on different rules.
lienredeemable_deed
lien_typescsvComma-separated lien types. Overrides lien_type.
lienredeemable_deed
property_typeenumUse of the underlying property.
residentialindustrialvacant_landvacant_commercialcommercialagriculturalresidential_homesteadother
property_typescsvComma-separated property types. Overrides property_type.
residentialindustrialvacant_landvacant_commercialcommercialagriculturalresidential_homesteadother
property_quality_gradescsvOur own condition grade for the property, A (best) through D.
ABCD
deal_typeenumHow the asking price was set against redemptive value: at par, at a discount, at a premium, or fixed by hand.
fixedpardiscountpremium
deal_typescsvComma-separated deal types. Overrides deal_type.
fixedpardiscountpremium
acquisition_sourcescsvWhere the certificate came from — a county auction, or the secondary market.
secondaryauction

Status and maturity

Leave status alone to get what is for sale.

ParameterMeaning
statusenum · default listedLifecycle status. listed also matches relisted, so the default returns everything currently on offer.
listedrelistedactivepurchasedredeemedcancelledforeclosure_eligibleforeclosure_initiatedforeclosedexpired
statusescsvComma-separated statuses. Overrides status.
listedrelistedactivepurchasedredeemedcancelledforeclosure_eligibleforeclosure_initiatedforeclosedexpired
maturity_afterdateMaturity on or after this date, YYYY-MM-DD. Filters expiration_date.
maturity_beforedateMaturity on or before this date, YYYY-MM-DD.
ParameterMeaning
searchstringFree text over street address, parcel ID, certificate number and county. 200 characters.
sort_byenum · default newest_firstSort order — the keys are in the next table.

Statuses the marketplace does not publish

pending_review, available, rejected are the pre-approval pipeline and are refused on this endpoint with a 400, on status and statuses alike — including when one rides along inside an otherwise valid comma-separated list. created_by_admin_id is refused the same way.

statuses=listed is not the same query as status=listed

The singular form expands: status=listed matches listed and relisted together. The plural is taken verbatim, so statuses=listed silently drops every relisted lien and returns fewer rows than the default did. Send statuses=listed,relisted if what you want is everything on offer.

Expired certificates are filtered out regardless of status

A lien whose expiration_date has passed is excluded from this endpoint unless it is a redeemable_deed. So the terminal statuses are queryable but thin — ?status=expired returns nothing — and this is not the endpoint to reconstruct a historical track record from. Use /market/activity for what has actually filled.

Sort keys

Passed as sort_by. The default is newest_first.

sort_byOrder
newest_firstMost recently added first, by record creation time. The default.
price_low_highCheapest asking price first.
price_high_lowMost expensive asking price first.
listing_price_low_highAlias of price_low_high, kept so existing bookmarks do not 400.
listing_price_high_lowAlias of price_high_low.
discount_low_highSmallest discount to redemptive value first.
discount_high_lowLargest discount to redemptive value first.
apy_low_highLowest net per-year rate first.
apy_high_lowHighest net per-year rate first. This ranks on the same figure the marketplace prints in its "Per year" column — net of our fee, annualized simply — and NOT on calculated.apy. Liens under 30 days from maturity have no per-year rate and are parked last rather than dropped.
maturity_soonestNearest maturity first.
maturity_latestFurthest maturity first.

Response shape

What the versioned tree wraps everything in, and what the curated tree does instead.

GET /liens
{
  "success": true,
  "data": [ /* the liens */ ],
  "meta": {
    "pagination": { "total": 257, "page": 1, "limit": 20, "totalPages": 13,
                    "hasNextPage": true, "hasPrevPage": false,
                    "nextPage": 2, "prevPage": null },
    "fee_config": { "interest_fee_bps": 1000, "fee_version": 1 },
    "filters":    { "status": "listed", "price_range": {}, "interest_range": {} },
    "sorting":    { "sort_by": "apy_high_low" }
  }
}

Read the fee from meta.fee_config rather than assuming 1000 bps — GET /liens/{id} carries the same object on the lien itself. It is read from the chain when a lien redeems, so it can change.

The /api/public tree does not use the envelope at all: it answers the object directly, or { error, message } with a 404 / 500.

Fields get added to these responses, so ignore what you do not recognize rather than failing on it.

Errors

Two layers refuse, and they do not refuse in the same shape.

A 400 comes back in one of two shapes, and it is worth handling both. Anything the route's own schema catches — a limit over 100, a non-numeric bound, an unknown status, a malformed id — is a framework validation error, and carries no success key at all:

schema-level 400
{ "statusCode": 400, "code": "FST_ERR_VALIDATION", "error": "Bad Request",
  "message": "querystring/limit must be <= 100" }

The cross-field and policy checks — a minimum above its maximum, a refused status, created_by_admin_id — use the envelope:

policy-level 400
{ "success": false, "message": "min_price must not exceed max_price" }

So branch on the status code, not on body.success.

Connecting over MCP

A hosted Model Context Protocol server on the same base URL. No credential, no Authorization header, nothing to install.

endpoint
POST https://api.lienfi.com/api/v1/mcp

JSON-RPC 2.0 over stateless streamable HTTP. There is no session to open and no session id to carry: every request is independent, so a client can call tools/call without having called initialize first. Only POST is served — every other verb answers 405 in JSON-RPC shape rather than falling through to the REST envelope.

Methods

MethodWhat it does
initializeServer name, version and the instructions string. Optional here — the transport is stateless, so nothing is negotiated and no session id comes back.
tools/listThe live tool set with JSON Schema for each. Trust this over any documentation, including this page.
tools/callRun one tool. params: { name, arguments }. The result is a text content block carrying JSON.
pingLiveness. Answers an empty result, and needs no prior initialize.
notifications/initializedAccepted and acknowledged so a spec-conformant client’s handshake completes. It is a notification, so there is nothing to read from it.

Adding the server

claude code
claude mcp add --transport http lienfi https://api.lienfi.com/api/v1/mcp

Any client that reads an mcpServers block — Claude Desktop, Cursor, a repo-local .mcp.json — takes the same thing as configuration:

mcpServers
{
  "mcpServers": {
    "lienfi": {
      "type": "http",
      "url": "https://api.lienfi.com/api/v1/mcp"
    }
  }
}

Or call it directly. The response is a JSON-RPC result whose content is a text block carrying JSON:

tools/list
curl -s -X POST 'https://api.lienfi.com/api/v1/mcp' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Trust tools/list over this page

The tool set is advertised live and is the authority on what exists. The descriptions here are maintained by hand — they cannot be imported into this app from the API — so if the two ever disagree, the wire is right.

When something goes wrong

A malformed request is a JSON-RPC error object — no HTTP envelope, no success key:

protocol error
{ "jsonrpc": "2.0", "id": 1,
  "error": { "code": -32600, "message": "..." } }

A tool that ran and refused is not an error at that layer. It answers a normal result with isError: true and a stable code inside the text block, so a program can branch on the reason rather than pattern-matching a sentence:

tool refusal
{ "content": [ { "type": "text",
    "text": "{\"error\":{\"code\":\"lien_not_found\",\"message\":\"...\"}}" } ],
  "isError": true }

Rate limit. 120 requests per minute per IP, which is generous enough that an agent listing tools and screening the book will not notice it. Over the budget you get a JSON-RPC error telling you to slow down, not an HTML page.

Nothing here can transact

This deployment is read-only. There is no purchase, quote or wallet-scoped tool — they are omitted from tools/list entirely rather than refusing per call, because a refusal invites a retry that cannot succeed. Nothing here signs, submits or reserves anything, and no request carries a key. A purchase needs a human at the marketplace.

MCP tools

Three tools, all unauthenticated. market_overview to see the shape of the book, search_liens to screen it, get_lien to inspect a candidate.

Inventory shape: how many liens are listed, which states and counties they are in, and the price / rate / LTV bounds.

Cheap, and the right first call. It is the same data as GET /liens/facets, and it turns "no results" from an ambiguous answer into a specific one.

Arguments

None. It takes an empty object.

Returns

  • totalHow many liens are on the live book at all.
  • states, counties, municipalitiesThe distinct values that actually have live inventory, each with a count. counties and municipalities carry their parent so you can narrow without a second call.
  • propertyTypes, qualityGradesThe distinct values on the live book, not the whole enum — and camelCase, unlike the REST tree’s snake_case query params.
  • rangesAn OBJECT, not top-level fields: listing_price, interest_rate, face_value, assessed_value and ltv_ratio, each { min, max } or null. Read a bound as ranges.listing_price.max.
tools/call
curl -s -X POST 'https://api.lienfi.com/api/v1/mcp' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"market_overview","arguments":{}}}'

Screen listed liens and return a shortlist ranked by NET yield — after our share of the gain — rather than by the gross APY the REST API publishes.

This is the tool worth using over GET /liens. It ranks on the same arithmetic this site displays, so you get our numbers rather than your reconstruction of them.

Scan wide, return narrow: it always screens a 100-row page and limit controls only the response size, because ranking on net yield is only meaningful over a decent sample.

Arguments

  • budget_usd number · optionalUpper bound on price. It filters the STORED listing_price column while a real quote recomputes the price from live redemptive value, so a lien inside this budget can still quote above it.
  • min_interest number · optionalMinimum statutory interest rate, percent.
  • max_ltv number · optionalMaximum loan-to-value, percent.
  • states string · optionalComma-separated two-letter codes, e.g. "FL,TX".
  • counties string · optionalComma-separated county names.
  • property_types string · optionalComma-separated, e.g. "residential,vacant_land".
  • limit number · optionalShortlist size, defaulting to 10. Out-of-range values are CLAMPED to 1–25 rather than refused, so limit: 1000 quietly returns 25. Applied to the ranked list and to maturing_soon SEPARATELY, so a full response can carry twice this many rows. The page scanned is always 100.

Returns

  • liensThe shortlist, ranked on net_apy descending.
  • maturing_soonLiens under 30 days from maturity, in their own list, ranked on net_return_pct. They carry no per-year rate at all.
  • scanned, ranked_returned, ranked_total, maturing_soon_returned, maturing_soon_totalFive counts — the page size scanned, plus returned and total for each of the two lists, which are capped independently. A truncated list otherwise reads as "these are all of them".
  • droppedLiens that could not be scored, counted rather than silently omitted. It also holds anything maturing within 24 hours, under no_remaining_term.
  • fee_config, noteThe live fee rate and the caveat that has to travel with any net figure.
  • apy_labelsWhat each rate on a row means, in the response itself, so a client never has to guess.
tools/call
curl -s -X POST 'https://api.lienfi.com/api/v1/mcp' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"search_liens","arguments":{"budget_usd": 5000, "min_interest": 12}}}'

The full record for one lien, including the redemptive-value projection and the fee rate that applies to it.

Use after search_liens to inspect a candidate.

Arguments

  • lien_id string · requiredThe lien UUID.

Returns

  • the whole lien rowEvery column GET /liens/{id} returns — around 60 of them, including transactions and the owner addresses. Only the projections are compacted; there is no field allowlist on this tool, unlike the rows search_liens returns.
  • calculated.redemptive_value_projection_summaryAnd ..._from_start_summary. Note the _summary suffix: the month-by-month ARRAYS are removed and these objects (from / to / change / points) take their place under a different key, deliberately, so code expecting an array cannot read .length off an object and treat a full projection as empty.
tools/call
curl -s -X POST 'https://api.lienfi.com/api/v1/mcp' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"get_lien","arguments":{"lien_id": "<lien-uuid>"}}}'

Why search_liens beats GET /liens for screening

It ranks on net yield using the same arithmetic this site displays, so the shortlist, the lien page and the marketplace column cannot report different money for one lien. Liens under a month from maturity come back in a separate maturing_soon list carrying no per-year rate — return × 365 / days is unbounded as the term falls, so annualizing one would put a trivial gain at the top of the ranking. Liens that cannot be scored at all are counted rather than silently dropped, and the projections are summarised to first/last/change so a single page does not fill your context.

Limits, CORS and caching

Read these before you write the client, not after the first CORS error.

CORS. The /api/public tree, /openapi.json and /mcp send Access-Control-Allow-Origin: * with credentials: false. The rest of /api/v1, including GET /liens, is on an exact-origin allowlist — so a browser fetch straight from your own domain will be blocked there. Call it server-side, or use the public tree.

Rate limits. The REST read routes are not rate limited per caller today; POST /mcp is, at 120 per minute per IP. Neither is a promise — it is a serverless deployment and platform limits still apply, so cache what you fetch and page rather than looping.

Caching. /api/public/liens/{id}, /liens/facets and /openapi.json send Cache-Control: public, max-age=300, and /market/activity sends max-age=30. Five minutes is short by the standards of data that moves monthly, so cache what you fetch rather than re-requesting it. Whether any given response also came from a CDN in front of us is not something the header will tell you, and not something to build on.

Versioning. Fields get added to these responses, so ignore what you do not recognize rather than failing on it. There is no published deprecation policy behind the v1 in the path — if you are building something you need to keep working, tell us and we will tell you before anything moves.

Machine-readable versions

The same material, for something that is not a person.

  • OpenAPI 3.1 — generated from the routes' own schema blocks. The admin, internal and webhook trees are withheld from it deliberately.
  • /llms.txt — the same conventions as prose for a model, and the entry point an agent should be given.
  • /sitemap.xml — every live lien page, each of which server-renders a record of the listing.

Before you summarize a listing

A tax lien is not a deposit and not a bond. Redemption is the expected outcome but not a guaranteed one, and the remedy if it does not occur is foreclosure on the property — slow, jurisdiction-specific, and not something this platform underwrites. Rates and deadlines are set by state statute and differ by state. Read the risk disclosure before presenting any of this as an investment recommendation, and how tax liens work for the statutory mechanics per state.