Thingery
Thingery is the open item database and MCP server for AI agents to list, find, and trade second-hand goods.
Say "sell this" to your AI assistant — it identifies the item, writes the listing, prices it, and publishes it. Say "find me a used camera under €300" — it searches every listing and proposes the best one. Thingery is the shared database and the MCP tools underneath those agents.
- Free for everyone. No take rate, no platform lock-in.
- Open source (Apache-2.0). Self-host the whole thing, or use the flagship instance.
- Agent-native: every capability is an MCP tool; any LLM can drive it.
- Self-hostable: Postgres + pgvector + Node 20.
Chinese-market name: 物坊 (Wùfāng).
MCP tools
Order flow
Orders close a deal on-platform. Stripe mode (buyer protection: platform charges the
buyer, holds funds, pays the seller after delivery + dispute window) runs when the deployment
has Stripe configured and the seller finished payout onboarding; otherwise orders run in
off-platform mode — Thingery tracks state and deadlines, parties settle payment themselves.
States: pending → sold → label_issued → in_transit → delivered → payout_pending → paid_out
with disputed/cancelled/refunded off-ramps; deadlines are enforced by a server-side sweep
(unpaid cancels 24h, dispute window 48h, never-shipped disputes auto-refund 72h — defaults,
env-tunable). Design record: ADR 0003.
Sellers authenticate with OAuth (one-click sign-in from any MCP host — see below) or a
thg_... API key (Authorization: Bearer); search is anonymous.
Buyer alerts
Saved intents feed buyer alerts: when a new listing matching an intent publishes, the match
shows up in my_intents' unread counts (a dedicated my_matches view is landing next).
Photo-required publishing
New listings need at least one photo to be visible to buyers — browse, search_items, and
category counts all exclude photoless listings (a single derived predicate; design record:
ADR 0005). What that means in practice:
create_listingwith zero images succeeds — the listing is created but pending: hidden from feeds, its item page shows placeholder art and isnoindex. The result carriesphoto_required,visible: false, andnext_stepspointing atadd_item_image(or the item page, where the owner can snap a photo in two taps).- The first photo publishes it — no second call needed.
add_item_imagereturnsvisible: true; pending items carryneeds_photo: trueeverywhere items are returned (get_item,my_listings, …). - Deleting the last photo returns the listing to pending (hidden again); pre-rollout listings are grandfathered and stay live without photos — on thingery.dev that means created before 2026-10-27; a self-hosted instance grandfathers whatever exists when it first deploys this version.
- Owners manage photos on the item page (upload, delete, reorder) via the session-cookie routes documented in docs/item-images.md.
Two ways to use it
- Zero-config (everyone): open
/chat.htmlon any deployment and talk to the Thingery assistant — describe an item to sell or to find. Searching needs no account; publishing needs a free one (/login.html, email + password; your MCP API key is shown after signup). The chat uses an LLM configured on the server (LLM_*env vars) — you bring the key, any OpenAI-compatible endpoint works; without it, chat is disabled while MCP and signup still work. - Bring your own agent (power users): point any MCP client at
/mcpand sign in with OAuth — Claude Desktop, ChatGPT connectors, VS Code and Inspector discover the sign-in flow automatically (RFC 9728 resource metadata + RFC 7591 dynamic client registration); you approve once on a consent screen and the host gets a rotatingthgo_token, with revoke-anytime at/apps. Prefer a key? The manualthg_...API key still works, and self-hosting is below (npx thingeryover stdio).
Quickstart
Pick your mode first — hosted and self-hosted expose the same MCP tools but not the same data: listings you create on the hosted service are visible to everyone; listings on your self-hosted instance live only in your database.
Mode A — hosted: search in one minute
No install. Add https://thingery.dev/mcp as a remote HTTP server in your MCP client
(Claude Desktop, ChatGPT connectors, VS Code, Inspector — see Connect an MCP client
below) and sign in with OAuth, or grab an API key at /login.html
(free account; the thg_... key is shown after signup). Searching needs no account at all —
verify the wire format with curl:
curl -s https://thingery.dev/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search_items","arguments":{"q":"Air Max"}}}'
Responses come back as Server-Sent Events; the JSON-RPC result is on the data: line.
Mode B — self-hosted: the npm package
The thingery npm package runs the whole server
against your own Postgres. No git clone needed:
mkdir my-thingery && cd my-thingery
npm install thingery
# 1. a Postgres with the pgvector extension (Docker: `docker run -d -p 5432:5432 \
# -e POSTGRES_PASSWORD=thingery pgvector/pgvector:pg16`; Homebrew: see below)
# 2. apply the shipped schema (idempotent, safe to re-run):
psql "postgres://postgres:thingery@localhost:5432/postgres" \
-f node_modules/thingery/db/schema.sql
# 3. run it (DATABASE_URL is the only required env var):
DATABASE_URL="postgres://postgres:thingery@localhost:5432/postgres" \
PORT=3000 node node_modules/thingery/dist/index.js
The server boots at http://localhost:3000 — /mcp, /browse, /healthz, and the HTTP
API. Create your account by opening /login.html —
your thg_... MCP key is shown right after signup. (On the pre-fix 0.1.2 release the
page 404'd — a packaging gap fixed after it; there you can still create the account
through the API — the key is returned exactly once, in the signup response:)
curl -s -X POST http://localhost:3000/api/signup \
-H 'content-type: application/json' \
-d '{"email":"[email protected]","password":"at-least-8-chars"}'
# → {"seller":{"name":"you","email":"…","api_key":"thg_…"}}
Then point any MCP client at http://localhost:3000/mcp with
Authorization: Bearer thg_... — or spawn it over stdio with the
npx config below.
Note: the npm package publishes at release cadence and can lag
mainon thingery.dev by days —/healthzreports the running version. If you need today's features, self-host from source instead:
Self-hosted from source (development)
git clone https://github.com/thingery/thingery
cd thingery
npm install
docker compose up -d db # Postgres 16 + pgvector
cp .env.example .env
npm run db:migrate
npm run db:seed # demo seller + 3 listings; prints an API key
npm run dev # http://localhost:3000
No Docker? Local Postgres
npm run db:migrate needs a Postgres with the vector extension — the schema creates it
unconditionally, even in keyword-only search mode. On macOS, Homebrew's pgvector bottle
supports PostgreSQL 17 and 18 (not 16, the version the Docker setup uses — any of them works
for Thingery):
brew install postgresql@18 pgvector
brew services start postgresql@18
psql postgres -c "CREATE ROLE thingery LOGIN PASSWORD 'thingery';" \
-c "CREATE DATABASE thingery OWNER thingery;"
psql -d thingery -c "CREATE EXTENSION vector;" # run as the Postgres superuser
The default DATABASE_URL in .env.example already matches this setup. On any other
Postgres, run the same two SQL statements as the superuser and point DATABASE_URL at it.
Connect an MCP client
Most hosts (Claude Desktop, ChatGPT connectors, VS Code, Inspector) can sign in with OAuth:
just add https://thingery.dev/mcp (or your self-hosted URL) as a remote HTTP server — the host
discovers the sign-in flow, you approve once, and tokens rotate automatically. On thingery.dev
you can review and revoke connected applications at /apps.
Hosts without OAuth support use a thg_... API key (from /login.html) as a static bearer
token instead:
{
"mcpServers": {
"thingery": {
"type": "http",
"url": "http://localhost:3000/mcp",
"headers": { "Authorization": "Bearer thg_YOUR_KEY" }
}
}
}
Omit the Authorization header for buyer-only (public search) usage — anonymous access to the
public read tools is always available. Design note: the decision record for the OAuth flow is
docs/adr/0002-mcp-oauth-onboarding.md; set
OAUTH_DISABLED=1 to turn OAuth routes off entirely (hosts then fall back to keys).
Run locally over stdio (npx)
Self-host without any web server — the package is thingery on npm
and runs as a local stdio MCP server against your own Postgres, spawnable by Claude Desktop and
similar clients:
{
"mcpServers": {
"thingery": {
"command": "npx",
"args": ["-y", "thingery"],
"env": {
"DATABASE_URL": "postgres://thingery:thingery@localhost:5432/thingery",
"THINGERY_API_KEY": "thg_YOUR_KEY"
}
}
}
}
THINGERY_API_KEY is optional; without it the server runs read-only (search + get). The
stdio server needs the same prerequisites as Mode B: a Postgres with the schema applied
(psql -f node_modules/thingery/db/schema.sql) and, for write tools, a key that exists in
that database (from that deployment's /login.html or /api/signup).
Troubleshooting
Try it with curl
# List tools (the Accept header is required by the MCP Streamable HTTP transport)
curl -s http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Search (finds the seeded Nike Air Max: €85 is within the €100 filter)
curl -s http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"search_items","arguments":{"q":"air max","max_price_cents":10000}}}'
Responses come back as Server-Sent Events; the JSON-RPC result is on the data: line.
Use an agent framework (LangChain, LlamaIndex, Composio)
Framework toolkits reach the same /mcp endpoint over streamable HTTP — copy-paste
quickstarts with verified end-to-end tool calls live in
examples/integrations/.
Configuration
Copy .env.example to .env. Key variables:
Order-flow windows and abuse caps (PAYMENT_DEADLINE_HOURS, DISPUTE_WINDOW_HOURS,
PICKUP_AUTOCONFIRM_DAYS, MAX_PENDING_ORDERS_PER_BUYER, …) are documented inline in
.env.example with their defaults.
Agent skills
skills/sell-item— the seller pipeline: photos → identified item → priced listingskills/find-item— the buyer pipeline: intent → scored matches → one best proposal
Data conventions
- Prices are integer cents:
12500= EUR 125.00 - Conditions:
new,like_new,good,fair,poor - Shipping tiers:
locker(shoebox-sized),parcel(≤30kg),bulky(furniture),local(pickup) - Machine discovery:
llms.txt·agents.json
Flexible schema
No fixed column set can cover sneakers, cameras and sofas at once, so the Item Graph is a hybrid:
- Rigid core columns for what every item shares: identity, price, condition, status, category, shipping estimation, embedding. These drive indexing and business logic.
attributes JSONBfor category-specific fields (size_eu,colorway,shutter_count,ISBN...), GIN-indexed for containment filters ({"size_eu": 43}) and flattened into the semantic embedding so they're meaning-searchable too.- A
categoriesregistry that declares expected attribute definitions per category.create_listingvalidates declared keys (types, required, enums) but unknown keys pass through — flexible by design, and federation-friendly since instances can diverge on attributes without breaking sync. Agents self-serve the schema viadescribe_category. sellers.profile JSONBfor user-side flexibility (locale, currency, notification prefs, negotiation mandates) — reserved for upcoming features.
Growth path: when an attribute becomes a hot filter, promote it to a Postgres generated column + index — no API change needed.
Which undeclared keys are actually in use is discoverable too: describe_category returns undeclared_in_use — undeclared attribute keys observed on active listings from multiple sellers, with item and distinct-seller counts. These keys are never validated (they pass through as-is), so treat them as search hints, not contracts — a signal of what buyers can filter on today.
Reading undeclared_in_use as an agent:
- It lists at most 20 keys, and only keys carried by ≥2 active listings from ≥2 distinct sellers. The field is omitted entirely when no key qualifies.
- Values are free-form: the same undeclared key can hold different values (even different types) on each listing. Use the key in
search_itemsattribute filters — they match any key, declared or not — but never treat one listing's value as the category's contract. - Writes are shape-capped on
create_listingandupdate_listing: at most 64 attribute entries, keys up to 128 chars, string values up to 500 chars. Over the cap, the call fails withinvalid_attributes.
Roadmap
Order state machine— shipped: orders with Stripe Connect payouts, carrier webhooks, disputes, and deadline sweeps (ADR 0003). Next: live-mode Stripe after legal/ToS sign-off.- Seller skill upgrade — photo capture shipped: frictionless item-page capture, placeholder art, and photo-gated publishing (ADR 0005). Comparable-based pricing is deferred until listings are numerous enough to be real comparables.
- Buyer alerts — next up: "new match for your intent" event stream.
- Federation protocol — instance-to-instance sync so anyone can host a node of the Item Graph. Deferred.
- Agent-to-agent negotiation — bounded-mandate negotiation between buyer and seller agents. Deferred.
We'll revisit federation and negotiation when there's real traction: more than 10 new listings per week for 2 consecutive weeks, or concrete pull from self-host adopters.
Security
What's verified and what isn't: docs/security-note.md. To report a vulnerability privately, see SECURITY.md.
Contributing
Built by an agent team with a human architect — see AGENTS.md for the working
conventions. PRs welcome; CI (typecheck, build, tests) must pass. New here? Start with
docs/community.md — good first issues, dev setup in under 10 minutes,
and where to ask questions.