# GalaxyIDX > MLS/IDX listings and lead capture for real-estate websites, built to be installed by AI coding agents. Listing pages render server-side on the customer's own domain, the MLS display rules are enforced by the API, and every key is limited to one account. Install via a WordPress plugin, server-rendered routes (Next.js, Astro, any stack) or the remote MCP server. Rules an agent must follow when installing: - Keep the secret key (`sk_…`) on the server only, in an environment variable (`IDX_SECRET_KEY`). Never in client code, a public env var, or source control. - Render listings server-side on the customer's domain. Never an iframe. - Show the listing firm, MLS name/logo, last-updated date and disclaimer with every listing. Never crop or overlay photos (`object-fit: contain`). - Never invent or estimate a listing fact; a missing value is shown as `—`. - Objective search filters only (Fair Housing). No SMS consent on lead forms. - The customer's broker must authorize the site with the MLS before listings appear; an agent cannot do that step. ## Docs - [Overview](https://docs.galaxyidx.com/docs/index.md): what it is, the install paths, prerequisites, price - [Quickstart: WordPress](https://docs.galaxyidx.com/docs/quickstart-wordpress.md): plugin install and checks - [Quickstart: Next.js and Astro](https://docs.galaxyidx.com/docs/quickstart-nextjs-astro.md): server-rendered routes and env vars - [Quickstart: any site](https://docs.galaxyidx.com/docs/quickstart-api.md): plain HTTP from any server stack - [Authentication](https://docs.galaxyidx.com/docs/authentication.md): secret vs publishable keys, errors - [Listings](https://docs.galaxyidx.com/docs/listings.md): search, detail, area stats, snapshot, field reference - [Leads and notifications](https://docs.galaxyidx.com/docs/leads.md): lead capture, email/CRM/webhook routing, webhook signatures - [MLS licensing and price](https://docs.galaxyidx.com/docs/mls-licensing.md): broker authorization, onboarding, $10/month per site + MLS fee at cost - [Compliance](https://docs.galaxyidx.com/docs/compliance.md): the display rules and what the API enforces - [SEO and AI search](https://docs.galaxyidx.com/docs/seo.md): one indexed page per listing, schema.org, sitemaps, area pages - [MCP server](https://docs.galaxyidx.com/docs/mcp.md): connect an AI agent; tool list ## API - [OpenAPI 3.1 spec](https://docs.galaxyidx.com/openapi.json): every endpoint, both key types, the error shape - [Agent skill](https://docs.galaxyidx.com/skill/SKILL.md): step-by-step install skill for AI coding agents ## Optional - [llms-full.txt](https://galaxyidx.com/llms-full.txt): all docs in one file --- Source: https://docs.galaxyidx.com/docs/index.md # GalaxyIDX GalaxyIDX puts MLS listings (IDX) and lead capture on a real-estate website. Listing pages render server-side on the site's own domain, so search engines and AI answer engines can read them. The product enforces the MLS display rules for you. It is written to be installed by an AI coding agent in one pass. ## How it fits together ``` MLS feed ──► GalaxyIDX API (https://api.galaxyidx.com/v1) ──► your site's server ──► HTML pages on your domain ▲ visitor lead form ──► your server route ──► POST /v1/leads ──► email / CRM / webhook ``` - **WordPress:** install the plugin. It renders the pages. → [quickstart-wordpress.md](https://docs.galaxyidx.com/docs/quickstart-wordpress.md) - **Next.js or Astro:** add a few server-rendered routes. → [quickstart-nextjs-astro.md](https://docs.galaxyidx.com/docs/quickstart-nextjs-astro.md) - **Anything else:** call the HTTP API from your server. → [quickstart-api.md](https://docs.galaxyidx.com/docs/quickstart-api.md) - **Your AI agent:** connect the MCP server and let it do the install. → [mcp.md](https://docs.galaxyidx.com/docs/mcp.md) ## Before you start 1. **An account and a site.** During early access, accounts are opened by request: email support@galaxyidx.com and we add the site's domain for you. 2. **MLS approval.** The customer's **broker** must authorize the site with the MLS. Until the MLS approves, the site stays `pending` and shows no listings. → [mls-licensing.md](https://docs.galaxyidx.com/docs/mls-licensing.md) 3. **Keys.** A **secret key** (`sk_…`) for server code and a **publishable key** (`pk_…`) if you post leads straight from the browser. → [authentication.md](https://docs.galaxyidx.com/docs/authentication.md) Price: $10/month per site for the software, plus the MLS's own fee passed through at cost. ## Reference - [listings.md](https://docs.galaxyidx.com/docs/listings.md): search, detail, area stats, snapshot - [leads.md](https://docs.galaxyidx.com/docs/leads.md): lead capture and notifications - [compliance.md](https://docs.galaxyidx.com/docs/compliance.md): the display rules and what the API enforces - [seo.md](https://docs.galaxyidx.com/docs/seo.md): how to make listing and area pages rank - [openapi.json](https://docs.galaxyidx.com/openapi.json): the full API spec Support: support@galaxyidx.com --- Source: https://docs.galaxyidx.com/docs/quickstart-wordpress.md # Quickstart: WordPress The GalaxyIDX plugin renders listing search, listing pages and lead forms in PHP, on your own domain. No iframe, no external widget. ## 1. Get your keys In https://app.galaxyidx.com: open your site and create a **secret key**. Copy it once; it is not shown again. Note the **site id**. The site must be `live` to show listings (it needs an approved MLS license, see [mls-licensing.md](https://docs.galaxyidx.com/docs/mls-licensing.md)). You can install while it is `pending`. ## 2. Install the plugin 1. Download the `galaxyidx` zip from https://app.galaxyidx.com. 2. WordPress admin → **Plugins → Add New → Upload Plugin** → choose the zip → **Install** → **Activate**. 3. Open the plugin's settings page. Paste the **secret key** and the **site id**. Save. The secret key is stored in WordPress options on your server. The plugin never prints it into a page. ## 3. Check it - Visit `/homes/` on your site: you should see listing cards with the listing firm and MLS attribution under each. - Open a listing: the page should load fast and show photos uncropped, the key facts, a tour/question form and the compliance footer. - View the page source: the listing text and the `application/ld+json` schema must be in the HTML. - Submit a test lead (use your own email). It should appear in `GET /v1/leads` and arrive at the address in your lead settings ([leads.md](https://docs.galaxyidx.com/docs/leads.md)). ## How lead forms work on WordPress Forms post to the plugin's own WordPress REST route. The plugin forwards the lead server-side with the secret key, so no publishable key is needed on WordPress. ## Do not - Do not paste the secret key into a theme file, a page, a block or JavaScript. - Do not build listing pages by hand or copy listing text into posts. The plugin keeps them fresh (the MLS requires at least every 12 hours) and removes them when they go off-market. - Do not crop listing photos with theme CSS (`object-fit: cover`), and do not put badges or text on them. - Do not add a second listings plugin that renders the same listings: one listing, one indexed page. --- Source: https://docs.galaxyidx.com/docs/quickstart-nextjs-astro.md # Quickstart: Next.js and Astro You add server-rendered routes that call the GalaxyIDX API with your **secret key**. The browser never sees the key. **Fastest path:** connect the MCP server ([mcp.md](https://docs.galaxyidx.com/docs/mcp.md)) and call `create_search_page` with `framework: "nextjs"` or `"astro"`. It returns every file below, complete. This page shows the core of it. ## 1. Environment variables (server only) ```bash IDX_SECRET_KEY=sk_live_… # from https://app.galaxyidx.com; set in your host's env settings, never commit it IDX_SITE_ID=site_… # the site id from the dashboard ``` Never prefix these with `NEXT_PUBLIC_` or `PUBLIC_`: that would ship the key to browsers. ## 2. A server-only API helper `lib/idx.ts` (Next.js) or `src/lib/idx.ts` (Astro; use `import.meta.env` instead of `process.env`): ```ts const API = 'https://api.galaxyidx.com/v1'; export const SITE_ID = process.env.IDX_SITE_ID ?? ''; export async function idx(path: string, params: Record = {}) { const url = new URL(API + path); for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== '') url.searchParams.set(k, String(v)); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.IDX_SECRET_KEY}` }, next: { revalidate: 3600 }, // Next.js: refresh hourly (the MLS requires at least every 12 hours) } as RequestInit); if (!res.ok) throw new Error((await res.json()).error?.code ?? `http_${res.status}`); return res.json(); } export const dash = (v: unknown) => (v === null || v === undefined || v === '' ? '—' : String(v)); ``` ## 3. Routes to add | Route | Data | Notes | |---|---|---| | `/homes/` | `GET /listings?site_id=` | Objective filters only: price, beds, status | | `/homes/--/` | `GET /listings/{listing_key}?site_id=` | Server-rendered; `notFound()` on 404; redirect to `home_site` when `is_home` is false; canonical and JSON-LD `url` are always absolute | | `/api/idx-lead` (POST) | `POST /leads` | Forwards the form with the secret key; passes the visitor IP in `X-Forwarded-For` only from a header your host sets (never the raw client header) | | `/areas//` (optional) | `GET /areas/{slug}/stats?site_id=` | Show each metric's period and source | ## 4. The listing page, in short (Next.js App Router) ```tsx import { notFound, redirect } from 'next/navigation'; import { idx, SITE_ID, dash } from '@/lib/idx'; export const revalidate = 3600; export default async function Page({ params }: { params: Promise<{ slug: string }> }) { const key = (await params).slug.split('-').pop()!; const l = await idx(`/listings/${encodeURIComponent(key)}`, { site_id: SITE_ID }).catch(() => notFound()); if (l.is_home === false) redirect(l.home_site.url); // one indexed page per listing const c = l.compliance; return (
{l.photos.map((p: any) => ( ))}

{l.address.display === false ? l.address.city : l.address.street}

{dash(l.price)} · {dash(l.beds)} beds · {dash(l.baths)} baths · {dash(l.sqft)} sq ft

{l.remarks_public ?? '—'}

Listing courtesy of {c.listing_firm} · {l.mls} · Last updated {c.last_updated}
{c.disclaimer}
); } ``` The full generated version adds the gallery alt text, JSON-LD, metadata, history, open houses and the lead form. ## Astro notes - Listing routes need on-demand rendering: `export const prerender = false;` plus your host's SSR adapter. - If you build fully static, rebuild at least every 12 hours (hourly is better) and fail the build if the API is down rather than shipping stale listings. ## Check it - View source on a listing page: the facts, the compliance footer and the JSON-LD are in the HTML. - Search the built client bundle for `sk_`: it must not appear. - Submit a test lead with your own email. --- Source: https://docs.galaxyidx.com/docs/quickstart-api.md # Quickstart: any site, via the HTTP API Use this for any stack (PHP, Rails, Django, Go, a static generator). Base URL: `https://api.galaxyidx.com/v1`. Full spec: [openapi.json](https://docs.galaxyidx.com/openapi.json). ## 1. Call from your server with the secret key ```bash curl -s "https://api.galaxyidx.com/v1/listings?site_id=$SITE_ID&status=active&limit=24" \ -H "Authorization: Bearer $IDX_SECRET_KEY" ``` ```json { "data": [ { "listing_key": "…", "price": 450000, "beds": 3, "baths": 2, "sqft": null, "address": { "city": "…", "display": true }, "photos": [ … ], "compliance": { "listing_firm": "…", "disclaimer": "…", "last_updated": "…" } } ], "next_cursor": "…" } ``` A `null` value means the MLS did not provide it. Show `—`. Never fill it in. ## 2. Render these pages on your server | Page | Call | |---|---| | Search `/homes/` | `GET /listings?site_id=&status=&min_price=&max_price=&beds=&limit=&cursor=` | | Listing `/homes/--/` | `GET /listings/{listing_key}?site_id=` | | Area `/areas//` | `GET /areas/{slug}/stats?site_id=` | Or fetch everything for one site at once, with caching: ```bash curl -s "https://api.galaxyidx.com/v1/sites/$SITE_ID/snapshot" -H "Authorization: Bearer $IDX_SECRET_KEY" \ -H 'If-None-Match: ""' # 304 = nothing changed ``` Refresh at least every 12 hours (hourly recommended). When a listing returns 404, remove its page at once. ## 3. Capture leads From your server (preferred): ```bash curl -s -X POST "https://api.galaxyidx.com/v1/leads" \ -H "Authorization: Bearer $IDX_SECRET_KEY" -H "Content-Type: application/json" \ -H "X-Forwarded-For: " \ -d '{"site_id":"'$SITE_ID'","action":"tour","listing_key":"…","name":"Test Buyer","email":"test@example.com", "tour":{"date":"2026-11-03","part_of_day":"morning"},"page_url":"https://your-site.example/homes/…/", "consent":{"email_marketing":false,"sms":false},"honeypot":""}' # → 201 {"id":"lead_…"} ``` Or straight from the browser with the **publishable** key (`pk_…`): same body, `Authorization: Bearer pk_…`. It only works from your site's registered domains. ## 4. Errors Every error is `{"error":{"code":"…","message":"…"}}`. See [authentication.md](https://docs.galaxyidx.com/docs/authentication.md). Then follow [compliance.md](https://docs.galaxyidx.com/docs/compliance.md) and [seo.md](https://docs.galaxyidx.com/docs/seo.md) for what each page must show. --- Source: https://docs.galaxyidx.com/docs/authentication.md # Authentication | Key | Looks like | Use it | Never | |---|---|---|---| | Secret | `sk_live_…` / `sk_test_…` | Server code only: WordPress PHP, server-side rendering, your lead route, the MCP server | In browser JS, a `NEXT_PUBLIC_`/`PUBLIC_` variable, a repo, a page, a log | | Publishable | `pk_live_…` / `pk_test_…` | Browser lead forms only (`POST /leads`) | Anything else; it cannot read listings or leads | Send either as a bearer token: ```http Authorization: Bearer sk_live_… ``` - A secret key belongs to **one account** (tenant) and can be limited to **one site**. Every answer is limited to that account; another account's ids return 404. - A publishable key works only when the browser's `Origin` is one of the site's registered `domains`, and it is rate-limited per key and visitor IP. - Keys are created in https://app.galaxyidx.com (site → Keys). The full key is shown **once**. We store only a hash and the first 8 characters. - Revoking a key takes effect immediately: calls fail with `401 key_revoked`. - Keep keys in environment variables (`IDX_SECRET_KEY`, `IDX_PUBLISHABLE_KEY`) or your host's secret store. ## Errors Every error has the same shape: ```json { "error": { "code": "key_revoked", "message": "This key was revoked." } } ``` | HTTP | Typical `code` | What to do | |---|---|---| | 400 | `invalid_request` | Fix the request; `message` says what | | 401 | `unauthorized`, `key_revoked` | Use a valid, unrevoked secret key | | 403 | `forbidden`, `origin_not_allowed`, `area_idx_disabled` | Wrong site for this key, Origin not registered, or a feature the MLS has not approved | | 404 | `not_found` | Not in your account, or the listing went off-market (remove its page) | | 429 | `rate_limited` | Wait for `Retry-After` seconds | Branch on the HTTP status and `code`, not on `message`. ## Dashboard sign-in People sign in to https://app.galaxyidx.com with an email magic link (valid 15 minutes, single use). The session lasts 30 days. There are no passwords. --- Source: https://docs.galaxyidx.com/docs/listings.md # Listings All listing calls need the **secret key** and a `site_id`. The site decides which listings you get: an `office` site shows only the account's own office listings; an `area` site (all brokers in an area) works only once the MLS has approved that domain for it. ## Search ```http GET https://api.galaxyidx.com/v1/listings?site_id=…&status=active&min_price=300000&max_price=600000&beds=3&limit=24&cursor=… ``` Returns `{ "data": [Listing…], "next_cursor": "…" | null }`. Pass `next_cursor` back as `cursor` for the next page. Filters are objective only: price, beds, status. The default order is organic (no paid ranking). ## One listing ```http GET https://api.galaxyidx.com/v1/listings/{listing_key}?site_id=… ``` Adds `home_site` (`domain`, `path`, `url`) and `is_home`. When an account has several sites, each listing has **one** home site, where its page is indexed. If `is_home` is `false`, link or redirect to `home_site.url`; do not publish a second indexable page. A `404` means the listing is gone (sold and aged out, withdrawn, or hidden by the seller). Remove its page right away. ## Listing fields | Field | Notes | |---|---| | `listing_key`, `mls`, `office_id`, `status` | Identity | | `price`, `beds`, `baths`, `sqft`, `acres`, `year_built` | `null` when the MLS has no value: show `—` | | `address` | `street`, `unit`, `city`, `state`, `postal_code`, `county`, `subdivision`, `display`. If `display` is `false`, the seller opted out: show no street, unit, map or coordinates | | `geo` | `lat`, `lng`. Treat as part of the address | | `remarks_public` | The public description, as written | | `photos[]` | `url`, `w`, `h`, `order`. Resize only (see [compliance.md](https://docs.galaxyidx.com/docs/compliance.md)). Sold listings carry only the primary photo | | `history[]` | Dated MLS events only | | `open_houses[]` | Only when the MLS feed has them | | `compliance` | `listing_firm`, `listing_firm_phone`, `listing_firm_email`, `mls_logo_url`, `disclaimer`, `last_updated`. Show with every listing | | `updated_at` | When this record last changed | Fields the MLS forbids on public sites (compensation, showing instructions, private remarks, contract and expiration dates) are never returned. ## Area statistics ```http GET https://api.galaxyidx.com/v1/areas/{slug}/stats?site_id=… ``` ```json { "area": { "slug": "…", "name": "…" }, "metrics": [ { "key": "median_list_price", "label": "Median list price", "value": 0, "unit": "USD", "period_from": "2026-09-01", "period_to": "2026-09-30", "source": "…" } ] } ``` Show `period_from`–`period_to` and `source` next to every number. ## Site snapshot ```http GET https://api.galaxyidx.com/v1/sites/{site_id}/snapshot If-None-Match: "" ``` Everything one site needs in one JSON document (site settings plus its listings), with an `ETag`. `304` means nothing changed. Good for static builds and plugins; poll at least every 12 hours. --- Source: https://docs.galaxyidx.com/docs/leads.md # Leads and notifications ## Capture a lead ```http POST https://api.galaxyidx.com/v1/leads Authorization: Bearer sk_… (from your server) or Bearer pk_… (from the browser, registered domains only) Content-Type: application/json ``` ```json { "site_id": "site_…", "action": "tour", "listing_key": "…", "name": "Test Buyer", "email": "test@example.com", "phone": null, "message": "Is the basement finished?", "tour": { "date": "2026-11-03", "part_of_day": "afternoon" }, "page_url": "https://your-site.example/homes/…/", "referrer": null, "utm": { "utm_source": "google" }, "consent": { "email_marketing": false, "sms": false }, "honeypot": "" } ``` Response: `201 {"id":"lead_…"}`, returned at once. Delivery happens in the background, with retries. | `action` | Use for | |---|---| | `tour` | Schedule a tour. `tour.date` + `morning`/`afternoon`/`evening`; it is a request, the agent confirms the time | | `ask` | A question about a listing | | `save` | Visitor saved a home (soft registration: name + email) | | `price_alert` | "Alert me if the price drops or it goes under contract" | | `register` | Soft registration prompt | | `contact` | General contact form | Form rules the API enforces: - `consent.sms` must be `false`. Do not show an SMS consent box. - `consent.email_marketing` defaults to `false`. If you show the opt-in, leave it unchecked. - `honeypot` is a hidden field people never fill. A filled value is dropped as spam. - Rate limits apply per key and visitor IP. When you forward from your server, pass the visitor IP in `X-Forwarded-For`, taken only from a header your host sets itself (or the socket address). Never pass on a client-supplied `X-Forwarded-For` unchanged: anyone could fake it to dodge the limit. If your host gives you no trusted IP, send none. - Ask for name and email only; phone is optional. Let visitors browse without registering. ## Where leads go Each lead fans out to the matching **lead route**. A site route overrides the account default. - **Email** to `email_to` (and `email_cc`). Subject: `New lead — `. Plain text + HTML. Reply-To is the lead's email, so replying reaches the buyer. - **CRM** (Lofty today), when our operators enable it for your account; the credential is stored by reference. - **Webhook**, signed (below), also enabled by our operators for now. Until the dashboard can store a CRM or webhook credential, self-serve accounts get email delivery only. New accounts start with the sign-up email as the default recipient. Change it any time: ```http GET https://api.galaxyidx.com/v1/lead-routes PUT https://api.galaxyidx.com/v1/lead-routes { "site_id": null, "email_to": ["leads@your-brokerage.example"], "email_cc": [], "lofty": null, "webhook": null } ``` `site_id: null` sets the account default; a site id sets that site's override. PUT replaces the whole route, so send the CRM and webhook settings you want to keep. A site with no route of its own uses the default, so a new site override must copy the default's CRM and webhook settings or those channels stop for that site. The MCP tool `set_lead_email` does both for you. Who may change what: - A **tenant-wide** secret key (or the dashboard) may change the default and any site's route. - A **site-scoped** secret key may change only its own site's route. `site_id: null` or another site's id is refused with 403. It reads only its own site's leads, and the default route plus its own route. ## Read leads ```http GET https://api.galaxyidx.com/v1/leads?site_id=…&since=2026-10-01T00:00:00Z ``` Each lead includes `deliveries[]` with `channel`, `status`, `attempts`, `last_error` and `at`. ## Verify a webhook We send `X-Signature: sha256=`: `sha256=` followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with the route's webhook secret. `X-Timestamp` repeats the body's `timestamp` field, which the signature covers; reject old timestamps to stop replays. ```js import crypto from 'node:crypto'; function signatureOk(rawBody, header) { const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(rawBody).digest('hex')); const got = Buffer.from(typeof header === 'string' ? header : ''); // timingSafeEqual throws on different lengths, so check the length first. return got.length === expected.length && crypto.timingSafeEqual(got, expected); } if (!signatureOk(rawBody, req.headers['x-signature'])) return res.status(401).end(); ``` ```python import hmac, hashlib, os ok = hmac.compare_digest("sha256=" + hmac.new(os.environ["WEBHOOK_SECRET"].encode(), raw_body, hashlib.sha256).hexdigest(), request.headers.get("X-Signature", "")) ``` Use the raw bytes, before any JSON parsing. Return 2xx quickly; we retry with backoff otherwise. --- Source: https://docs.galaxyidx.com/docs/mls-licensing.md # MLS licensing, onboarding and price MLS listing data is licensed. Each MLS has to approve GalaxyIDX as a data vendor, and **your broker** has to authorize your site with that MLS before it can show listings. An AI agent can do the install, but it cannot do this step: a person at the brokerage signs. ## Who must authorize The **broker of record** of the brokerage the agent belongs to, who is a participant of the MLS. An agent alone cannot authorize IDX for their site. ## Onboarding steps 1. **Request an account** by emailing support@galaxyidx.com (early access is by request). The account starts as a `trial`. 2. **Add the site** (its domain and every alternate domain). 3. **The broker of record signs** your MLS's IDX authorization for the site. A pre-filled form with e-signature is planned and not built yet. 4. **Request the MLS authorization** in the dashboard: choose the MLS and the office id(s), and add a reference to the signed authorization (for example a document id). The license shows `pending_broker`, then `submitted` once we file it with the MLS. 5. **The MLS approves.** Our operators mark the license `approved` with the approved domains. Nobody can do this through the API. 6. **The site goes live** when the account is active (or on an internal plan) and an approved license covers the site. While a site is `pending`, install everything and test with the API; the listing calls return no listings yet. ## What a site may show - **Office sites** (`scope: office`) show only your own office's listings. This is the default. - **Area sites** (`scope: area`) show all brokers' listings for an area. They need the MLS to approve that exact domain, and they are switched on per MLS. ## Price - **Software:** $10/month per site. - **MLS fee:** whatever the MLS charges for the data, passed through **at cost** as its own line (it can be $0). Checkout shows both lines. Billing runs on Stripe; the account turns `active` when payment succeeds. ## Your responsibilities - Keep your own MLS membership current. - Don't edit listing facts on your pages, and keep the pages connected (we keep them fresh and compliant). - Handle the leads you receive under your own privacy policy. To delete a person's lead data on request, contact support. Questions: support@galaxyidx.com --- Source: https://docs.galaxyidx.com/docs/compliance.md # Compliance: the rules GalaxyIDX enforces These rules come from the MLS IDX rules (the NAR IDX model most MLSs follow) and each MLS's own IDX policy. The API enforces the data rules server-side, so no client can switch them off. Your pages must follow the display rules. ## Enforced by the API (you get compliant data) - **Freshness.** Listings refresh at least every 12 hours (deltas arrive hourly). A listing that goes off-market, or whose "display on internet" flag turns off, disappears from the API immediately (404). - **Never returned:** compensation, showing instructions, private/agent remarks, listing contract, on-market and expiration dates, and expired, withdrawn or on-hold listings. - **Seller opt-outs.** When the seller opted out of showing the address, `address.display` is `false` and the street is withheld. When the seller turned off comments or automated valuations, those features stay off. - **Sold listings** carry only the primary photo. - **Scope.** An office site gets only its own office's listings; all-broker area data only reaches domains the MLS approved. - **Domain lock.** Publishable keys only work from the site's registered domains. - **No SMS.** Lead consent for texting is always off. ## Your pages must 1. **Attribute every listing display** (cards included): the listing firm name, at least as large as your normal body text; the MLS name and logo; the last-updated date; the MLS disclaimer; and the listing firm's phone/email when present. Every listing includes all of this in `compliance`. 2. **Show photos resized only.** Use `object-fit: contain` on a neutral background. No cropping (`object-fit: cover` crops), no overlays, no text, badges or watermarks on a photo. 3. **Show facts exactly as delivered.** A missing value is `—`. Never estimate, round up, rewrite or invent a fact, and never generate a description that adds facts. 4. **Hide what the seller hid.** If `address.display` is `false`: no street, unit, map pin or coordinates anywhere, including schema and social cards. 5. **Use objective search only.** Filters by geography, price, beds, type. Default sort is organic. No paid placement. 6. **Follow Fair Housing.** No "safe", "family-friendly", "good schools", crime, demographic, religious or similar descriptions, filters or AI answers. For schools or crime, link to official sources and let the buyer decide. 7. **Leave indexing on, block scraping.** Search engines may index listing pages. Don't publish bulk downloads or unauthenticated JSON dumps of listings. 8. **Ask for little.** Let visitors browse freely. Saving, alerts and tours may ask for name + email (phone optional). No SMS consent box. Email marketing opt-in unchecked by default. ## If you use AI on listing pages Answer only from the listing's own fields. Refuse safety, demographic and school-quality questions and point to official sources. Don't mass-produce near-identical pages. --- Source: https://docs.galaxyidx.com/docs/seo.md # SEO and AI search Listings only help a site rank if search engines can read them on **your** domain. That is the point of GalaxyIDX. ## The rules that matter 1. **Server-render on your own domain.** The listing facts, description and compliance block must be in the HTML response. No iframe, no subdomain, no client-only widget. 2. **One indexed page per listing.** Use `/homes/--/`. If you run several sites, only the listing's `home_site` publishes the page (`is_home: true`); the others link to it. 3. **Canonical** each listing page to its `home_site.url`. Filtered and paged search URLs canonicalize to the plain `/homes/` page. 4. **Structured data (JSON-LD)** built from the listing's own facts only: - `RealEstateListing` with `url`, `name`, `description`, `image`, `dateModified` - `offers`: `Offer` with `price` and `priceCurrency` - `mainEntity`: `SingleFamilyResidence` when the property type says so (otherwise `Accommodation`) with `numberOfBedrooms`, `numberOfBathroomsTotal`, `floorSize`, `yearBuilt` - `PostalAddress`, plus `GeoCoordinates` only when `address.display` is not `false` - `BreadcrumbList`, and your own `RealEstateAgent` / organization block from your site settings - Never put contract dates or anything not in the listing into schema. 5. **Sitemaps.** List active listing pages, area pages and sold pages in separate sitemaps. Ping IndexNow when a page is added or removed. 6. **Off-market pages.** When the API returns 404, return 404 or 410 at once. A sold listing may keep a short page (primary photo only) that links to similar homes and the area page; noindex it after 30 days, then return 410. 7. **Area pages are your main ranking pages.** Use `GET /areas/{slug}/stats` (cite the period and source beside each number), add real local writing of your own, and list current homes. Don't mass-produce near-identical area pages. 8. **Speed.** Preload the first photo, lazy-load the rest, load the map only on tap, and set width/height on images. Aim for LCP under 2 s on mobile. 9. **Social cards.** `og:image` = the primary photo, uncropped, with nothing drawn on it. 10. **AI answer engines.** Publish `/llms.txt` on your site describing your areas and linking to your area and listing index pages. Keep any AI summary short and drawn only from listing facts. ## Quick checks - `curl -s https://your-site.example/homes// | grep -c application/ld+json` returns at least 1. - The listing price and listing firm name appear in the raw HTML. - Only one of your domains returns 200 + indexable for a given listing. --- Source: https://docs.galaxyidx.com/docs/mcp.md # MCP server: let your AI agent do the install GalaxyIDX runs a remote MCP server (Streamable HTTP) at: ``` https://mcp.galaxyidx.com/mcp ``` It authenticates with your **secret key** as a bearer token. Every tool works only inside your own account. ## Connect Claude Code: ```bash claude mcp add --transport http galaxyidx https://mcp.galaxyidx.com/mcp --header 'Authorization: Bearer ${IDX_SECRET_KEY}' ``` Use **single quotes**. They store the reference `${IDX_SECRET_KEY}`, which the client expands when it connects. Double quotes make your shell paste the real key into the saved MCP config, and with `--scope project` that config is a `.mcp.json` file that gets committed. Never save a config that holds a literal key. Any client that takes a JSON config (keep the key in an environment variable, not in the file): ```json { "mcpServers": { "galaxyidx": { "type": "http", "url": "https://mcp.galaxyidx.com/mcp", "headers": { "Authorization": "Bearer ${IDX_SECRET_KEY}" } } } } ``` Check your client's docs for how it expands environment variables in headers. ## Tools | Tool | What it does | |---|---| | `install_instructions(framework)` | The compliant install steps for `wordpress`, `nextjs`, `astro` or `other`, your sites, and the env vars to set. Call first | | `search_listings(site_id, …)` | Search the listings a site may show (objective filters) | | `get_listing(listing_key, site_id)` | One listing, with `home_site` / `is_home` | | `get_market_stats(area_slug, site_id)` | Area metrics with period and source | | `create_search_page(framework, site_id)` | Page spec + ready files for `/homes/` and listing pages | | `create_area_page(framework, site_id, area_slug)` | Page spec + files for an area page | | `list_leads(site_id?, since?)` | Your leads and their delivery status | | `get_lead_settings(site_id?)` | Who receives new-lead emails | | `set_lead_email(email_to, email_cc?, site_id? \| account_default: true)` | Change who receives new-lead emails for one site, or (with `account_default: true`) the account default. CRM and webhook delivery are kept; confirm the addresses with the user first | Generated files read the key from `IDX_SECRET_KEY` and the site from `IDX_SITE_ID`. They never contain a key. ## Protocol MCP revision `2026-07-28` (stateless, per-request `_meta`, `server/discover`), with fallback for clients on `2025-11-25`, `2025-06-18` and `2025-03-26` (`initialize`). POST only. Rate-limited per key and per client IP; repeated rejected keys from one IP are refused for a minute. ## Site-scoped keys A secret key can be limited to one site. With such a key the tools see only that site and its leads, and `set_lead_email` can change only that site's route. The account default route needs a tenant-wide key or the dashboard. The API enforces this (403); it is not just a client-side check. --- Source: https://docs.galaxyidx.com/skill/SKILL.md --- name: idx-listings-install description: Add MLS/IDX real-estate listings and lead capture to a website with GalaxyIDX, the compliant way. Use when asked to "add MLS listings", "add IDX", "show my listings on my site", "add a home search", "add listing pages" or "capture buyer leads" on a real-estate agent or brokerage site built with WordPress, Next.js, Astro or any server stack. Covers keys, server-side rendering, MLS attribution, photo rules, Fair Housing, SEO schema and lead email routing. --- # Install GalaxyIDX (MLS/IDX listings + lead capture) Docs index: https://docs.galaxyidx.com/llms.txt · API: https://api.galaxyidx.com/v1 · MCP: https://mcp.galaxyidx.com/mcp ## 0. Before writing code 1. Find the framework: `wp-config.php` / `wp-content/` = **WordPress**; `next.config.*` = **Next.js**; `astro.config.*` = **Astro**; anything else = **other** (HTTP API). 2. Ask the user for (never invent them): - the **secret key** (`sk_live_…` / `sk_test_…`). They set it in the host's environment as `IDX_SECRET_KEY`; you never print, echo, log or commit it. - the **site id**, set as `IDX_SITE_ID`. - who should receive new-lead emails. 3. If they have no account: have them email support@galaxyidx.com to request an account (early access is by request). Tell them plainly that their **broker** must sign the MLS authorization before listings appear (https://docs.galaxyidx.com/docs/mls-licensing.md). You cannot do that step. Price: $10/month per site plus the MLS fee at cost. 4. If the MCP server is connected, call `install_instructions(framework)` and use its `your_sites` and steps. ## 1. Install by framework **WordPress** 1. Install and activate the `galaxyidx` plugin (zip from the dashboard). 2. Have the user paste the secret key and site id into the plugin settings. Do not write the key into any PHP, theme or JS file. 3. Do not hand-build listing pages or add a second listings plugin. **Next.js (App Router) / Astro** 1. Prefer the MCP tool `create_search_page(framework, site_id)`; write the returned `files` as given. Without MCP, follow https://docs.galaxyidx.com/docs/quickstart-nextjs-astro.md. 2. Keep the API helper server-only. Never use `NEXT_PUBLIC_` / `PUBLIC_` for the key. 3. Revalidate at most every hour (`export const revalidate = 3600`). Astro: on-demand rendering (`prerender = false` + an SSR adapter), or rebuild at least every 12 hours. 4. Add the lead route (`/api/idx-lead`) that forwards to `POST /v1/leads` with the secret key and `X-Forwarded-For`. 5. Optional: `create_area_page(framework, site_id, area_slug)` for each area the site covers. **Other stacks** 1. Call `https://api.galaxyidx.com/v1` from the server with `Authorization: Bearer $` + `IDX_SECRET_KEY`. Spec: https://docs.galaxyidx.com/openapi.json. 2. Build `/homes/` (search), `/homes/--/` (detail) and a server-side lead forwarder. ## 2. Lead emails Call `set_lead_email(email_to=[…], site_id=…)` for one site, or `set_lead_email(email_to=[…], account_default=true)` for the account default (MCP), or `PUT /v1/lead-routes` with the addresses the user gave you. Confirm them with the user before saving. The tool keeps CRM and webhook delivery; a raw PUT replaces the whole route, so send them back yourself. ## 3. Rules you must keep (the MLS can cut off the feed otherwise) - Server-render listing HTML on the customer's own domain. No iframe, no client-only widget, no subdomain. - Every listing display (cards too): listing firm (normal text size or larger), MLS name + logo, last-updated date, disclaimer, firm phone/email when present. All of it is in `listing.compliance`. - Photos: `object-fit: contain` on a neutral background. Never `cover`, never crop, never text/badges/watermarks on a photo. - Missing facts render as `—`. Never estimate, rewrite or invent listing facts, alt text or descriptions. - `address.display === false`: no street, unit, map or coordinates anywhere (page, schema, social card). - On 404 from the API, the listing is gone: return 404 at once; never serve a cached copy. - `is_home === false`: link/redirect to `home_site.url`; one indexed page per listing. - Fair Housing: objective filters only (price, beds, status, geography). No "safe", "family-friendly", school-quality, crime or demographic wording, filters or AI answers; link to official sources instead. - Lead forms: name + email, phone optional, no SMS consent box, email opt-in unchecked by default, a hidden honeypot field. ## 4. SEO JSON-LD `RealEstateListing` + `Offer` + `PostalAddress` (+ `GeoCoordinates` only when the address is shown) + `BreadcrumbList`, from listing facts only; canonical = `home_site.url`; separate sitemaps for listings and area pages. Details: https://docs.galaxyidx.com/docs/seo.md. ## 5. Verify before you say "done" - [ ] `curl` a listing page: price, listing firm and `application/ld+json` are in the raw HTML. - [ ] `grep -r "sk_live_\|sk_test_"` over the repo and the built client bundle finds nothing. - [ ] Photos are uncropped on a narrow (phone) viewport. - [ ] A test lead with the user's own email returns 201 and shows up in `list_leads`. - [ ] Report to the user: site status (`pending` until the MLS approves), where leads go, and anything they still must do (broker authorization, env vars on the host).