Docs and API
Every home is one LLC, and that LLC’s membership is one whole token. This page explains how that works and how to build on it. The read API is open, needs no key, and answers cross-origin, so a listing site or a portfolio tracker can integrate in an afternoon.
The model
A home is moved into a single-purpose limited liability company. That LLC holds the deed, and its membership is represented by a single ERC-20 token with zero decimals and a fixed supply of one. The token is the home: it cannot be split into shares, and the contract has no mint function, so no second unit can ever exist.
That makes integration simple. A balance is 0 or 1. Owning the token means owning the whole home, and there is no cap table to reason about, no dust, and no rounding.
| Property | Value |
|---|---|
| decimals | Always 0 |
| totalSupply | 1 while the home is tokenized, 0 after redemption |
| Quote asset | USDC |
| Settlement | UniswapX limit orders signed through Permit2 |
How a token is named
Both fields are derived from the property, never typed by a registrar, so they are consistent across the catalogue and cannot collide.
| Field | Example | Rule |
|---|---|---|
| name | US-IL-CHI-17172150241205 | Country, state, a three-letter city code, and the assessor’s parcel number. |
| symbol | CHI241205 | City code plus the tail of the same parcel number. Never over 11 characters. |
The name is keyed to the parcel number rather than an MLS number on purpose. An MLS number is unique only within one MLS, and a listing that goes off market and comes back gets a new one; the parcel number is the county’s permanent id and is what the deed references. Where a home has no parcel number on file the name falls back to its ZIP and address, which is a signal that the record is incomplete.
The ticker is keyed to the parcel too, rather than to the unit. A unit number is unique only inside one building, so a ticker built from it would collide across a city and have to be broken with a counter, which would make the result depend on which home happened to be tokenized first. The parcel tail belongs to the home, so the same home always produces the same ticker. Where two tails would clash, the window widens to take another digit from the parcel rather than appending a counter.
The ticker stays short because wallets and explorers truncate symbols at around eleven characters. If you need a stable key, use the contract address: that is the identifier the chain guarantees. Two homes can never share one.
Instant Buy and Bid Only
A listing is in one of two states, and both are in the same API.
| State | What it means |
|---|---|
| Instant Buy | status: "tokenized" and a non-null token. The contract exists, so an order can be signed and filled today. |
| Bid Only | status: "pending" and token: null. There is nothing onchain to trade yet, so a bid is a signed commitment that converts into a limit order when the home is tokenized. |
Both are biddable. If you are building a listings surface, show them together and switch the call to action on whether token is null.
The token contract
Standard ERC-20 plus a redemption mechanic. Everything you need to integrate is in the first four functions.
function decimals() view returns (uint8) // always 0
function totalSupply() view returns (uint256)
function balanceOf(address) view returns (uint256) // 0 or 1
function symbol() view returns (string)
function llcName() view returns (string) // the entity that holds the deed
function propertyURI() view returns (string) // points back at this app's record
function redemptionAddress() view returns (address)
function redemptionCount() view returns (uint256)
function registrar() view returns (address)
function transfer(address to, uint256 value) returns (bool)
function redeem(uint256 amount) // same as transferring to redemptionAddress
event Transfer(address indexed from, address indexed to, uint256 value);
event RedemptionRequested(
uint256 indexed redemptionId,
address indexed holder,
uint256 amount,
bool fullOwnership
);propertyURI() is the important one for integrators: it resolves to this app’s record for the home, so a token found in a wallet is self-describing. Fetch it and you get the address, the photos, the LLC and the current price without knowing anything else.
Redemption
Sending the token to redemptionAddress() burns it and emits RedemptionRequested. Because the supply is one, that event always carries fullOwnership: true. The claim is then settled off-chain by the registrar as an assignment of the LLC’s membership interests, subject to the operating agreement and applicable law. It is not automatic. See the Terms for what that does and does not guarantee.
API reference
Base URL is this deployment’s origin. Read endpoints need no authentication, send access-control-allow-origin: *, and return JSON. Write endpoints are described below and are called from a wallet, not a server.
| Endpoint | Purpose |
|---|---|
| GET /api/properties | Every listing, tokenized and bid-only |
| GET /api/properties/:slug | One listing, plus live onchain state |
| GET /api/orders/book?slug= | Best bid and ask, depth and levels |
| GET /api/orders?slug= | Individual signed orders |
| GET /api/offers?slug= | Open bids on a home with no token yet |
| POST /api/orders/quote | Build the payload a wallet signs |
| POST /api/orders | Submit the signature |
| POST /api/offers | Prepare and submit a bid |
| GET /api/config | Chain id, USDC and Permit2 addresses |
| GET /api/health | Database, chain config and RPC source |
Listings
curl https://your-deployment/api/properties
curl "https://your-deployment/api/properties?status=pending" # bid-only homes
curl "https://your-deployment/api/properties?q=chicago" # searchEach property looks like this. Fields that are not known are null, never invented.
{
"slug": "123-s-green-st-1202b-chicago-il",
"status": "tokenized", // or "pending" for bid-only
"street": "123 S Green St",
"unit": "1202B",
"city": "Chicago", "state": "IL", "zip": "60607",
"neighborhood": "Greektown / West Loop",
"county": "Cook County", "apn": "17172150241205",
"propertyType": "Condo",
"beds": 1, "baths": 1, "sqft": 765, "yearBuilt": 2008,
"valuationUsd": 365000,
"rentEstimateUsd": 2178,
"photos": ["https://…"],
"llcName": "USDC Homes 1202B LLC",
"llcState": "DE",
"llcFormationId": null, // null means not filed yet
"deedUri": null,
"facts": { "walkScore": 98, "unitFeatures": ["In-unit washer/dryer"] },
"sources": { "redfin": "https://…", "capturedAt": "2026-08-22" },
"token": { // null when the home is bid-only
"contractAddress": "0x…",
"chainId": 1,
"symbol": "GRN1202B",
"totalSupply": 1,
"priceUsdc": 365000,
"redemptionAddress": "0x…"
}
}Rendering both states in a third-party app is one branch:
const { properties } = await fetch(
"https://your-deployment/api/properties",
).then((r) => r.json());
for (const home of properties) {
if (home.token) {
// Instant Buy: quote, sign, submit
show(home, `Buy for ${home.token.priceUsdc} USDC`);
} else {
// Bid Only: no contract yet, take a signed commitment
show(home, "Place a bid");
}
}GET /api/properties/:slug returns the same object under property, plus an onChain object read live from the node (supply, redemption address, registrar, redemption count), or null when the token is not deployed or the RPC is unreachable.
Order book
curl "https://your-deployment/api/orders/book?slug=123-s-green-st-1202b-chicago-il"
{
"token": { "address": "0x…", "symbol": "GRN1202B", "chainId": 1,
"totalSupply": 1, "indicativePriceUsdc": 365000 },
"bestBid": 360000, "bestAsk": 365000, "spread": 5000,
"bidDepthTokens": 1, "bidNotionalUsdc": 360000,
"bids": [{ "price": 360000, "quantity": 1, "orders": 1 }],
"asks": [{ "price": 365000, "quantity": 1, "orders": 1 }],
"orders": [ /* individual signed orders */ ]
}You can address a token by slug or by token=0x…, which is the easier one if you started from a contract address in a wallet.
Bids on a home with no token
curl "https://your-deployment/api/offers?slug=125-s-green-st-1205a-chicago-il"
{
"settlement": "bid",
"bestOfferUsdc": 380000,
"offers": [
{ "bidderAddress": "0x…", "amountUsdc": 380000,
"deadline": 1790000000, "status": "open", "digest": "0x…" }
]
}Buying a tokenized home
Two steps, and the server never holds anything. Ask for the payload, have the wallet sign it, send the signature back. The buyer’s USDC stays in their wallet until a filler settles the order against the UniswapX reactor.
// 1. Ask for exactly what the wallet should sign
const params = {
slug: "123-s-green-st-1202b-chicago-il",
side: "bid", // "ask" to sell
quantity: 1, // a home is one token
pricePerToken: "365000", // USDC for the whole home
swapper: walletAddress,
deadline: Math.floor(Date.now() / 1000) + 7 * 86400,
};
const quote = await fetch("https://your-deployment/api/orders/quote", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(params),
}).then((r) => r.json());
// 2. Sign it and hand back the signature
const signature = await wallet.signTypedData(quote.typedData);
await fetch("https://your-deployment/api/orders", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...params, nonce: quote.nonce,
deadline: quote.deadline, signature }),
});The server rebuilds the order from those primitives rather than trusting a client-supplied struct, so an altered price simply fails signature verification. Before the first bid the buyer must approve USDC for Permit2 (/api/config gives you both addresses); a seller approves the home token instead.
Cancelling is POST /api/orders/:hash/cancel with a maker-signed message. That pulls the order from this book. To make the signature unfillable everywhere, the maker must also invalidate the Permit2 nonce onchain.
Bidding on an untokenized home
Same shape, one endpoint. Post without a signature to get the typed data, then post again with it. The bid is an EIP-712 Bid under our own domain, not an escrow and not an onchain order.
const params = {
slug: "125-s-green-st-1205a-chicago-il",
bidder: walletAddress,
amountUsdc: "380000",
deadline: Math.floor(Date.now() / 1000) + 30 * 86400,
};
const prepared = await post("/api/offers", params); // no signature yet
const signature = await wallet.signTypedData(prepared.typedData);
await post("/api/offers", { ...params, nonce: prepared.nonce, signature });Withdrawing is DELETE /api/offers/:digest with a signed USDC Homes: withdraw bid <digest> message.
Reading a token onchain
You do not need this API to check ownership. A home token is a normal ERC-20, so any wallet or indexer already understands it, and propertyURI() takes you back to the full record.
import { createPublicClient, http, parseAbi } from "viem";
const abi = parseAbi([
"function balanceOf(address) view returns (uint256)",
"function propertyURI() view returns (string)",
"function llcName() view returns (string)",
]);
const client = createPublicClient({ transport: http(RPC_URL) });
const owns = await client.readContract({
address: TOKEN, abi, functionName: "balanceOf", args: [holder],
});
if (owns === 1n) {
const uri = await client.readContract({ address: TOKEN, abi, functionName: "propertyURI" });
const { property } = await fetch(uri).then((r) => r.json());
// property.street, property.photos, property.valuationUsd, …
}To watch for a home changing hands, subscribe to Transfer. To watch for a redemption, subscribe to RedemptionRequested: with a supply of one it always means the whole home was claimed.
Notes and limits
- Rate limits. Import and analyze endpoints are limited per IP. Read endpoints are not currently limited, but please cache: listing data changes slowly.
- Stability. These endpoints are versionless and may change. Fields are added more often than removed. Treat unknown fields as ignorable rather than fatal.
- Data quality. Listing facts come from public sources and automated extraction, and may be incomplete or out of date. A field that is null was not stated by the source, not guessed. Verify anything you rely on.
- Not an endorsement. Integrating this data is not a recommendation to transact, and neither we nor the data are a substitute for your own diligence. See the risk disclosures.
- Verify contracts. Always check a contract address against
/api/properties/:slugbefore letting anyone interact with it.