USDC Homes

Docs and API

Last updated 22 August 2026

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.

PropertyValue
decimalsAlways 0
totalSupply1 while the home is tokenized, 0 after redemption
Quote assetUSDC
SettlementUniswapX 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.

FieldExampleRule
nameUS-IL-CHI-17172150241205Country, state, a three-letter city code, and the assessor’s parcel number.
symbolCHI241205City 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.

StateWhat it means
Instant Buystatus: "tokenized" and a non-null token. The contract exists, so an order can be signed and filled today.
Bid Onlystatus: "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.

EndpointPurpose
GET /api/propertiesEvery listing, tokenized and bid-only
GET /api/properties/:slugOne 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/quoteBuild the payload a wallet signs
POST /api/ordersSubmit the signature
POST /api/offersPrepare and submit a bid
GET /api/configChain id, USDC and Permit2 addresses
GET /api/healthDatabase, 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"        # search

Each 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