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.
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, thenGET /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_liensranks 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.
https://api.lienfi.com/api/v1https://api.lienfi.com/api/publicThe 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.
# 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.
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.
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
idpath · 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.
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
idpath · uuid · requiredThe lien id.
Response
A flat object, or { error, message } with a 404 / 500. No success key either way.
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.
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
idpath · uuid · requiredThe lien id.buyerquery · 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.
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
idpath · uuid · requiredThe lien id.
Response
An image URL, or a null-ish payload for a property with no resolvable panorama.
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
idpath · uuid · requiredThe lien id.
Response
An image URL.
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
limitquery · integerEvents to return, 1–25. Defaults to 10.include_summaryquery · booleanAdds lifetime sale totals tometa. Defaults to false.
Response
Recent fills with their prices and lots.
curl -s 'https://api.lienfi.com/api/v1/market/activity?limit=10&include_summary=true'GET/legal
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
typequery · enum · requiredThe document slug, e.g.terms-and-conditions.
Response
The document body, its version and its effective date.
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.
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.
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.
| Parameter | Meaning |
|---|---|
pageinteger · default 1 | Page number, 1-based. |
limitinteger · default 20 | Rows 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.
| Parameter | Meaning |
|---|---|
min_pricenumber | Minimum asking price, USD. |
max_pricenumber | Maximum asking price, USD. |
min_face_valuenumber | Minimum face value — the delinquent tax the certificate was struck for, not what it sells for. |
max_face_valuenumber | Maximum face value, USD. |
min_assessed_valuenumber | Minimum county-assessed value of the underlying property, USD. |
max_assessed_valuenumber | Maximum county-assessed value, USD. |
min_interestnumber | Minimum 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_interestnumber | Maximum statutory interest rate, percent. |
min_ltvnumber | Minimum loan-to-value, percent. Accepts 0–999.99; that ceiling is the column width, not a business rule. |
max_ltvnumber | Maximum loan-to-value, percent. |
Location
Call /liens/facets first — it returns the states and counties that actually have inventory, so you are not guessing.
| Parameter | Meaning |
|---|---|
statestring | One two-letter state code. |
statescsv | Up to 10 state codes. Overrides state. |
countystring | One county name, matched exactly. |
countiescsv | Up to 50 county names. Overrides county. |
municipalitystring | One municipality name. Null on much of the book — prefer county. |
municipalitiescsv | Up to 50 municipality names. Overrides municipality. |
Property and terms
Enumerated. Anything outside the listed values is a 400.
| Parameter | Meaning |
|---|---|
lien_typeenum | Certificate or redeemable deed. The two accrue and mature on different rules.lienredeemable_deed |
lien_typescsv | Comma-separated lien types. Overrides lien_type.lienredeemable_deed |
property_typeenum | Use of the underlying property.residentialindustrialvacant_landvacant_commercialcommercialagriculturalresidential_homesteadother |
property_typescsv | Comma-separated property types. Overrides property_type.residentialindustrialvacant_landvacant_commercialcommercialagriculturalresidential_homesteadother |
property_quality_gradescsv | Our own condition grade for the property, A (best) through D.ABCD |
deal_typeenum | How the asking price was set against redemptive value: at par, at a discount, at a premium, or fixed by hand.fixedpardiscountpremium |
deal_typescsv | Comma-separated deal types. Overrides deal_type.fixedpardiscountpremium |
acquisition_sourcescsv | Where the certificate came from — a county auction, or the secondary market.secondaryauction |
Status and maturity
Leave status alone to get what is for sale.
| Parameter | Meaning |
|---|---|
statusenum · default listed | Lifecycle status. listed also matches relisted, so the default returns everything currently on offer.listedrelistedactivepurchasedredeemedcancelledforeclosure_eligibleforeclosure_initiatedforeclosedexpired |
statusescsv | Comma-separated statuses. Overrides status.listedrelistedactivepurchasedredeemedcancelledforeclosure_eligibleforeclosure_initiatedforeclosedexpired |
maturity_afterdate | Maturity on or after this date, YYYY-MM-DD. Filters expiration_date. |
maturity_beforedate | Maturity on or before this date, YYYY-MM-DD. |
| Parameter | Meaning |
|---|---|
searchstring | Free text over street address, parcel ID, certificate number and county. 200 characters. |
sort_byenum · default newest_first | Sort 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_by | Order |
|---|---|
newest_first | Most recently added first, by record creation time. The default. |
price_low_high | Cheapest asking price first. |
price_high_low | Most expensive asking price first. |
listing_price_low_high | Alias of price_low_high, kept so existing bookmarks do not 400. |
listing_price_high_low | Alias of price_high_low. |
discount_low_high | Smallest discount to redemptive value first. |
discount_high_low | Largest discount to redemptive value first. |
apy_low_high | Lowest net per-year rate first. |
apy_high_low | Highest 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_soonest | Nearest maturity first. |
maturity_latest | Furthest maturity first. |
Response shape
What the versioned tree wraps everything in, and what the curated tree does instead.
{
"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:
{ "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:
{ "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.
POST https://api.lienfi.com/api/v1/mcpJSON-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
| Method | What it does |
|---|---|
initialize | Server name, version and the instructions string. Optional here — the transport is stateless, so nothing is negotiated and no session id comes back. |
tools/list | The live tool set with JSON Schema for each. Trust this over any documentation, including this page. |
tools/call | Run one tool. params: { name, arguments }. The result is a text content block carrying JSON. |
ping | Liveness. Answers an empty result, and needs no prior initialize. |
notifications/initialized | Accepted 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 mcp add --transport http lienfi https://api.lienfi.com/api/v1/mcpAny client that reads an mcpServers block — Claude Desktop, Cursor, a repo-local .mcp.json — takes the same thing as configuration:
{
"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:
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:
{ "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:
{ "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.countiesandmunicipalitiescarry 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_valueandltv_ratio, each{ min, max }or null. Read a bound asranges.listing_price.max.
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_usdnumber · 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_interestnumber · optionalMinimum statutory interest rate, percent.max_ltvnumber · optionalMaximum loan-to-value, percent.statesstring · optionalComma-separated two-letter codes, e.g. "FL,TX".countiesstring · optionalComma-separated county names.property_typesstring · optionalComma-separated, e.g. "residential,vacant_land".limitnumber · optionalShortlist size, defaulting to 10. Out-of-range values are CLAMPED to 1–25 rather than refused, solimit: 1000quietly 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 onnet_apydescending.maturing_soonLiens under 30 days from maturity, in their own list, ranked onnet_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, underno_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.
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_idstring · requiredThe lien UUID.
Returns
the whole lien rowEvery columnGET /liens/{id}returns — around 60 of them, includingtransactionsand the owner addresses. Only the projections are compacted; there is no field allowlist on this tool, unlike the rowssearch_liensreturns.calculated.redemptive_value_projection_summaryAnd..._from_start_summary. Note the_summarysuffix: 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.lengthoff an object and treat a full projection as empty.
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.