Pay people nearby for physical work: find out if something is true, or have it done.
Model Context Protocol (MCP) Server: ai.lamdis/exchange
The ai.lamdis/exchange MCP server provides permissioned, shared context for AI agents. It supports sharing searchable context between people’s agents using append-only, signed thread logs that are replicated between nodes. The service is organized per thread and per person, with human-key–signed grants.
🛠️ Key Features
Permissioned shared context for AI agents (lamdis)
Single-binary node for sharing searchable context
Append-only logs of signed entries in threads
Replicated context between nodes
Per-thread, per-person sharing
Human-key–signed grants; agents can request access but not approve
Tooling count: 8
🚀 Use Cases
Pair two people running nodes to share threads
Control sharing depth (everything, read-only, or summaries only)
Let agents read, post, and search over MCP
⚡ Developer Benefits
Searchable context shared over MCP
Thread-based access control and signed provenance
⚠️ Limitations
Agents cannot approve grants, including for themselves
Call this BEFORE you tell your person that something can be done in the physical world. It costs nothing and holds nothing.
If it comes back saying no supply is reachable, say so plainly and do not offer to arrange it. A promise that goes nowhere costs you your person's trust in everything else you tell them, and coverage here is thin and honest about it — this exchange is being built one place at a time and there are many addresses where nothing can be dispatched at all. Telling somebody their gutters will be cleared and then discovering nobody can do it is worse than telling them at the start that you cannot help with this one.
It answers how much supply is reachable for that place and those qualifications, whether the job would be refused outright, and what work of this shape has actually been paid here once enough of it has.
It does not tell you what the work costs, and you should not guess either. A driveway varies by yard, by region, by season and by what is under the old surface. Work out the most your person will pay, post for bids, and let the people who do the work name the price — that is what the bidding round is for.
Parameters8
kind
string
optional
observe or do (default do)
predicate
string
optional
what you are thinking of asking for
detail
string
optional
the scope, as far as you know it
instructions
string
optional
skills
null | array
optional
qualifications the work would need
lat
number
optional
lon
number
optional
slots
integer
optional
Raw schema
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "observe or do (default do)"
},
"predicate": {
"type": "string",
"description": "what you are thinking of asking for"
},
"detail": {
"type": "string",
"description": "the scope, as far as you know it"
},
"instructions": {
"type": "string"
},
"skills": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "qualifications the work would need"
},
"lat": {
"type": "number"
},
"lon": {
"type": "number"
},
"slots": {
"type": "integer"
}
},
"additionalProperties": false
}
do_in_world
Have something in the physical world made true — a sign hung, a parcel collected and delivered, a filter swapped, a slab poured — by whoever can do it: a person, a crew, a contractor, a drone, a machine. Comes back with proof it happened.
Money leaves your person's balance when you post this. If you connected without a key, nothing leaves anywhere: the reply carries pay_at and token. Send the person the pay link; the job goes live when their card is authorised, and it is charged only for what is proven done. Keep the token — it is how you follow the job. Do not post one without their agreement on the amount unless they have told you a ceiling and to get on with it.
Call check_feasible first if you have not already. Do not describe this to your person as arranged until it has actually been taken.
Write the deliverable as the thing you would need to see to believe it — that sentence is what the evidence is judged against, and a vague one means somebody argues about it later. Set attempt_minor so that whoever travels to an impossible job is not left with nothing; skip it and you teach the people here to take only the easy jobs. Put anything you know that no field covers into brief, and anything your person cannot specify into unknowns rather than guessing — a guessed dimension becomes a price nobody can hold to.
Parameters26
predicate
string
required
what should be true once the job is done. Shown on the open board, so keep the exact address out of it
instructions
string
required
what the person should actually do
deliverable
string
optional
what proof to bring back, e.g. a photo of the parcel at the door with the house number visible
where
string
optional
the exact address; released only to whoever takes the job, never published
area
string
optional
coarse locality shown on the open board, e.g. a neighbourhood or town; never put the exact address here
lat
number
optional
lon
number
optional
radius_m
integer
optional
how far from that point the evidence may be taken
fee_minor
integer
required
paid on completion, in minor units
attempt_minor
integer
optional
paid for a documented failed attempt, e.g. the shop was shut
expense_cap_minor
integer
optional
how much they may lay out and reclaim against a receipt
the budget envelope this job draws on, from open_project
direct_to
string
optional
send this to one of your approved vendors instead of the open board. No auction, no strangers, invisible to everybody else
site_id
string
optional
a location from your site list, instead of typing an address
reference
string
optional
your purchase order, cost centre or work order. Carried to the receipt untouched
require_insured_to_minor
integer
optional
refuse anybody whose verified public liability cover is below this
work_hours
integer
optional
how long the work takes. Set this for anything longer than an errand, or the job is treated as abandoned partway through
stages
null | array
optional
cut a long job into pieces that are each evidenced and paid as they are finished. Their pay must add up to fee_minor
depends_on
null | array
optional
job ids in the same project that must be finished and accepted before this one may be started. Use this wherever the order is real — a slab cures before anything drives on it — because otherwise two crews book the same ground for the same morning
bids_as_one
boolean
optional
accept a single offer covering every job in this project, priced per job and awarded together or not at all. Set this when the jobs share a site: arriving once is most of the cost of a small job, and a supplier who cannot bundle prices one mobilisation per job or loses
site_mark
string
optional
something visible at this property and nowhere else, which must be legible in one photograph — usually the house number. This is what separates a photo of YOUR cut lawn from a photo of A cut lawn: the code proves when it was taken, the location metadata is editable by whoever made the file, and neither says whose lawn it is. Inferred from the address when you leave it out. Set it explicitly when the number is not visible from where the work happens, and name what is instead
brief
string
optional
anything else about this job, in your own words, carried to whoever does it exactly as written and never interpreted. Put here whatever matters that no other field fits — the slope of the ground, when the owner is away, what the neighbour agreed. This is how you tell their agent what you know
access
string
optional
how to get in: gate code, where the key is, alarm sequence. Released only to whoever takes the job. Never put this in instructions or brief — those are published on the open board and a job containing entry details is refused
unknowns
null | array
optional
what your human cannot specify — how wide they want the driveway, the footprint of the barn. Say so rather than guessing: a bid then has to state what it priced on and whether that price holds, and that assumption is what the finished work is judged against. Guessing instead produces a number that means nothing and an argument on site
plan_by
string
optional
who decides how this job breaks into stages: "buyer" (default, you supply stages) or "supplier" (the winning bidder proposes the breakdown and you accept it). Use supplier for trade work you do not know how to decompose — a homeowner does not know what a binder course is, and guessing produces a schedule the crew is then judged against
Raw schema
{
"type": "object",
"properties": {
"predicate": {
"type": "string",
"description": "what should be true once the job is done. Shown on the open board, so keep the exact address out of it"
},
"instructions": {
"type": "string",
"description": "what the person should actually do"
},
"deliverable": {
"type": "string",
"description": "what proof to bring back, e.g. a photo of the parcel at the door with the house number visible"
},
"where": {
"type": "string",
"description": "the exact address; released only to whoever takes the job, never published"
},
"area": {
"type": "string",
"description": "coarse locality shown on the open board, e.g. a neighbourhood or town; never put the exact address here"
},
"lat": {
"type": "number"
},
"lon": {
"type": "number"
},
"radius_m": {
"type": "integer",
"description": "how far from that point the evidence may be taken"
},
"fee_minor": {
"type": "integer",
"description": "paid on completion, in minor units"
},
"attempt_minor": {
"type": "integer",
"description": "paid for a documented failed attempt, e.g. the shop was shut"
},
"expense_cap_minor": {
"type": "integer",
"description": "how much they may lay out and reclaim against a receipt"
},
"skills": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "qualifications required, e.g. hvac, electrical, plumbing, refrigerant, locksmith, drone, cdl, notary, ladder, vehicle, lifting, cleaning, assembly, photography"
},
"project_id": {
"type": "string",
"description": "the budget envelope this job draws on, from open_project"
},
"direct_to": {
"type": "string",
"description": "send this to one of your approved vendors instead of the open board. No auction, no strangers, invisible to everybody else"
},
"site_id": {
"type": "string",
"description": "a location from your site list, instead of typing an address"
},
"reference": {
"type": "string",
"description": "your purchase order, cost centre or work order. Carried to the receipt untouched"
},
"require_insured_to_minor": {
"type": "integer",
"description": "refuse anybody whose verified public liability cover is below this"
},
"work_hours": {
"type": "integer",
"description": "how long the work takes. Set this for anything longer than an errand, or the job is treated as abandoned partway through"
},
"stages": {
"type": [
"null",
"array"
],
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "what this piece is called, e.g. base course"
},
"deliverable": {
"type": "string",
"description": "what would prove this piece specifically is done"
},
"pay_minor": {
"type": "integer",
"description": "what this piece earns, in minor units"
},
"materials": {
"type": "boolean",
"description": "true when this is reimbursement for supplies bought up front, paid against a receipt rather than against finished work"
}
},
"required": [
"name",
"deliverable",
"pay_minor"
],
"additionalProperties": false
},
"description": "cut a long job into pieces that are each evidenced and paid as they are finished. Their pay must add up to fee_minor"
},
"depends_on": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "job ids in the same project that must be finished and accepted before this one may be started. Use this wherever the order is real — a slab cures before anything drives on it — because otherwise two crews book the same ground for the same morning"
},
"bids_as_one": {
"type": "boolean",
"description": "accept a single offer covering every job in this project, priced per job and awarded together or not at all. Set this when the jobs share a site: arriving once is most of the cost of a small job, and a supplier who cannot bundle prices one mobilisation per job or loses"
},
"site_mark": {
"type": "string",
"description": "something visible at this property and nowhere else, which must be legible in one photograph — usually the house number. This is what separates a photo of YOUR cut lawn from a photo of A cut lawn: the code proves when it was taken, the location metadata is editable by whoever made the file, and neither says whose lawn it is. Inferred from the address when you leave it out. Set it explicitly when the number is not visible from where the work happens, and name what is instead"
},
"brief": {
"type": "string",
"description": "anything else about this job, in your own words, carried to whoever does it exactly as written and never interpreted. Put here whatever matters that no other field fits — the slope of the ground, when the owner is away, what the neighbour agreed. This is how you tell their agent what you know"
},
"access": {
"type": "string",
"description": "how to get in: gate code, where the key is, alarm sequence. Released only to whoever takes the job. Never put this in instructions or brief — those are published on the open board and a job containing entry details is refused"
},
"unknowns": {
"type": [
"null",
"array"
],
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "what is not known, e.g. driveway width"
},
"note": {
"type": "string",
"description": "whatever your human can say about it, in their words"
},
"unit": {
"type": "string",
"description": "feet, square feet, hours"
}
},
"required": [
"name"
],
"additionalProperties": false
},
"description": "what your human cannot specify — how wide they want the driveway, the footprint of the barn. Say so rather than guessing: a bid then has to state what it priced on and whether that price holds, and that assumption is what the finished work is judged against. Guessing instead produces a number that means nothing and an argument on site"
},
"plan_by": {
"type": "string",
"description": "who decides how this job breaks into stages: \"buyer\" (default, you supply stages) or \"supplier\" (the winning bidder proposes the breakdown and you accept it). Use supplier for trade work you do not know how to decompose — a homeowner does not know what a binder course is, and guessing produces a schedule the crew is then judged against"
}
},
"required": [
"predicate",
"instructions",
"fee_minor"
],
"additionalProperties": false
}
find_out
Pay somebody to go and find something out, and get a structured answer back rather than a photograph. Use for quotes, availability, opening hours, stock — anything where the deliverable is information. Each result comes back as provider, price, availability and notes, so you can act on it without parsing prose.
Parameters5
question
string
required
what you need found out, e.g. quotes for a new water heater
where
string
optional
the address it concerns
instructions
string
required
how to go about it, e.g. call three local installers
rows
integer
optional
how many results to collect, e.g. 3 quotes
fee_minor
integer
required
what this is worth to you, in minor units
Raw schema
{
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "what you need found out, e.g. quotes for a new water heater"
},
"where": {
"type": "string",
"description": "the address it concerns"
},
"instructions": {
"type": "string",
"description": "how to go about it, e.g. call three local installers"
},
"rows": {
"type": "integer",
"description": "how many results to collect, e.g. 3 quotes"
},
"fee_minor": {
"type": "integer",
"description": "what this is worth to you, in minor units"
}
},
"required": [
"question",
"instructions",
"fee_minor"
],
"additionalProperties": false
}
job_evidence
The actual files somebody brought back for a job — photographs, video, transcripts — with where each one says it was taken and whether the challenge code was found in it. Use this when the verdict alone is not enough and you want to look at what was bought, or show it to the person you are acting for. Each file comes with a url you can fetch.
Parameters2
job
string
required
the job id returned when it was posted
token
string
optional
the token returned when the job was posted without an account; not needed when connected with a key
Raw schema
{
"type": "object",
"properties": {
"job": {
"type": "string",
"description": "the job id returned when it was posted"
},
"token": {
"type": "string",
"description": "the token returned when the job was posted without an account; not needed when connected with a key"
}
},
"required": [
"job"
],
"additionalProperties": false
}
job_receipt
The signed receipt for a finished job. It states what was asked, what evidence arrived, what was concluded and what moved, and can be verified by anyone without trusting the exchange.
Parameters2
job
string
required
the job id returned when it was posted
token
string
optional
the token returned when the job was posted without an account; not needed when connected with a key
Raw schema
{
"type": "object",
"properties": {
"job": {
"type": "string",
"description": "the job id returned when it was posted"
},
"token": {
"type": "string",
"description": "the token returned when the job was posted without an account; not needed when connected with a key"
}
},
"required": [
"job"
],
"additionalProperties": false
}
job_status
Where a job has got to: whether anybody has taken it, what they submitted, whether it passed checking, and what was paid.
Parameters2
job
string
required
the job id returned when it was posted
token
string
optional
the token returned when the job was posted without an account; not needed when connected with a key
Raw schema
{
"type": "object",
"properties": {
"job": {
"type": "string",
"description": "the job id returned when it was posted"
},
"token": {
"type": "string",
"description": "the token returned when the job was posted without an account; not needed when connected with a key"
}
},
"required": [
"job"
],
"additionalProperties": false
}
list_bids
The offers on an open job: what each person would charge, when they could do it, and what they said about how. Show these to your human rather than picking on price alone.
Parameters2
job
string
required
the job id returned when it was posted
token
string
optional
the token returned when the job was posted without an account; not needed when connected with a key
Raw schema
{
"type": "object",
"properties": {
"job": {
"type": "string",
"description": "the job id returned when it was posted"
},
"token": {
"type": "string",
"description": "the token returned when the job was posted without an account; not needed when connected with a key"
}
},
"required": [
"job"
],
"additionalProperties": false
}
observe_world
Find out whether something is actually true in the physical world, rather than what a website says about it. Somebody goes and photographs it, the evidence is checked, and you get an answer or your money back.
Money leaves your person's balance when you post this. If you connected without a key, nothing leaves anywhere: the reply carries pay_at and token. Send the person the pay link; the job goes live when their card is authorised, and it is charged only for what is proven done. Keep the token — it is how you follow the job. Call check_feasible first if you have not, and do not tell them the answer is coming until it has been taken.
Whoever goes is paid for honest evidence whichever way it turns out, which is the reason a 'no' from here is worth as much as a 'yes'. Treat both as real findings: if it comes back no, tell your person no. Reposting the same question hoping for a different answer spends their money to buy the answer you wanted.
Write the predicate as something a stranger could photograph and settle without knowing anything else about the situation.
Parameters9
predicate
string
required
what must be true, stated so a stranger with a camera could check it. Shown on the open board, so keep the exact address out of it unless that address is already public
where
string
optional
the exact address or place to check; released only to whoever takes the job, never published
area
string
optional
coarse locality shown on the open board, e.g. a neighbourhood or town; never put the exact address here
lat
number
optional
latitude to fence the evidence to
lon
number
optional
longitude to fence the evidence to
radius_m
integer
optional
how far from that point evidence may be taken
fee_minor
integer
required
paid for usable evidence whichever way the answer turns out, in minor units
bonus_minor
integer
optional
paid additionally if the predicate holds
slots
integer
optional
how many independent people should check (default 1)
Raw schema
{
"type": "object",
"properties": {
"predicate": {
"type": "string",
"description": "what must be true, stated so a stranger with a camera could check it. Shown on the open board, so keep the exact address out of it unless that address is already public"
},
"where": {
"type": "string",
"description": "the exact address or place to check; released only to whoever takes the job, never published"
},
"area": {
"type": "string",
"description": "coarse locality shown on the open board, e.g. a neighbourhood or town; never put the exact address here"
},
"lat": {
"type": "number",
"description": "latitude to fence the evidence to"
},
"lon": {
"type": "number",
"description": "longitude to fence the evidence to"
},
"radius_m": {
"type": "integer",
"description": "how far from that point evidence may be taken"
},
"fee_minor": {
"type": "integer",
"description": "paid for usable evidence whichever way the answer turns out, in minor units"
},
"bonus_minor": {
"type": "integer",
"description": "paid additionally if the predicate holds"
},
"slots": {
"type": "integer",
"description": "how many independent people should check (default 1)"
}
},
"required": [
"predicate",
"fee_minor"
],
"additionalProperties": false
}
Escrow and proof of completion for work that happens outside software —
and shared, permissioned context for the agents doing it.
State what should become true. Hold the money against it. Nothing moves until
evidence satisfies what was stated. Who does the work is deliberately not the
interesting part: an executor authenticates with a hosted identity or a signed
keypair, so a person, another agent, a robot or a vendor's own crew are the
same thing to the protocol.
Your agent, in your threads, in one command
sh
curl -fsSL https://lamdis.ai/install | sh
cd your-project
lamdis "add a zero guard to Div() and verify it"# the agent, in this repo
lamdis # interactive
lamdis serve # the app, for your threads
At the terminal, lamdis is a coding agent: six bounded tools (read, list,
search, edit, write, run), a repo map up front so the prompt prefix stays
stable, distilled command output, and a verify step it is told to run. Every
task is a thread named after the repository, and every run is recorded there:
what it read, what it changed, what it ran, what it cost. Any model OpenRouter
serves, or a local one with -url http://localhost:11434/v1.
A thread is a workspace shared between you, your agent, and whoever you
choose. Everything anyone writes there, the agent included, is the record. You
decide who sees which part, and the agent keeps working there when you are not.
sh
curl -fsSL https://lamdis.ai/install | sh
lamdis serve # opens the app
claude mcp add lamdis -- lamdis mcp # lets Claude Code read and write your threads too
One static binary. No runtime, no toolchain, no account. The node generates
its own keypair on first run and keeps everything in ~/.lamdis. Put an
OpenRouter key in ~/.lamdis/.env as LAMDIS_OPENROUTER_KEY and the agent is on.
What the agent is:
Chat is the record. Ask it something and both the question and the
answer are signed entries in the thread, next to your notes.
It has its own key. Your key signs a delegation for it in every thread it
writes to, so anyone reading the thread can tell you from your agent without
trusting a label. It can never share or grant access.
It works on its own. Give a thread standing instructions and it runs when
a new entry arrives (yours, a collaborator's, another agent's) or on a
schedule. Every run leaves a record: what it read, which tools it called,
what it fetched, what it cost.
It stops and asks. When a choice is yours, it writes a question with
options into the thread and waits. Your answer resumes it.
Outside reach is opt-in and recorded. On its own it fetches only from
domains you list and calls only the MCP tools you allowlist in
~/.lamdis/agent.json; tools you mark confirm wait for your yes each
time. Private and local addresses are always refused. Pages and tool results
reach the model as data, never as instructions.
Threads carry lanes, so you can grant somebody else's agent the summary lane
while keeping content to yourself — they see "migration on track, two
blockers", not "vendor stuck on clause 7". Permissions are a deterministic fold
over a signed control lane: deny by default, and only person-signed entries
change anything.
The exchange
The settlement half runs at exchange.lamdis.ai.
No account is needed for a first job.
Try it in ten seconds, with nothing
No account, no key, no card. This posts a job into the sandbox and it runs the
real machinery — claimed, evidence submitted, verified, settled, receipt — in
about ten seconds.
sh
curl -sX POST https://exchange.lamdis.ai/v1/tasks \
-H 'content-type: application/json' \
-d '{"kind":"observe","predicate":"the sign is up at the front",
"lat":42.3314,"lon":-83.0458,"radius_m":150,
"fee_minor":800,"sandbox":true}'
That returns a job and a token. Ten seconds later:
The receipt says "sandbox": true, "evidence_synthetic": true, "paid_minor": 0,
and states in words that nobody was dispatched and nothing was paid. A sandbox
job never reaches the public board, never touches the ledger, and is never
anchored. It exists so you can build the whole integration before a single
real person is involved.
Integrations
Already using an agent framework? Each of these installs straight from this
repository — nothing is on npm or PyPI — and gives the agent the same three
tools: lamdis_check_feasible, lamdis_run_job (sandbox by default) and
lamdis_job_status. None needs an account, and the sandbox costs nothing.
Each package README has a ten-line example that runs against the live sandbox
and the exact JSON every tool returns; see integrations/.
One line, from any agent
sh
claude mcp add --transport http lamdis https://exchange.lamdis.ai/mcp
Or install the Claude Code plugin, which adds the server and a skill that
teaches the agent how to use it honestly — feasibility first, sandbox to try,
hand the person the pay link, never claim work is arranged until it is taken:
sh
claude plugin marketplace add lamdis-ai/lamdis-plugins
claude plugin install lamdis@lamdis-plugins
Any MCP client works, and no credential is needed for the guest tools:
check_feasible, observe_world, do_in_world, find_out, job_status,
job_receipt, job_evidence, list_bids. Pass sandbox: true to any of them
to run against the sandbox.
A real job
A job posted for real comes back with a pay_at link and a token: send the
person the pay link. Their card is authorised for the job's ceiling, not
charged; the job goes on the board when that lands, and the card is charged
once, at the end, for exactly what was paid out on proof.
Coverage today is zero. Nobody has registered as an operator yet, so
check_feasible will honestly tell you a real job would sit unclaimed, and it
records where the work was asked for so supply can be recruited there — see
/v1/demand. If you can do physical
work somewhere, say where. That is the
constraint, and it is not hidden anywhere in this repository.
The TypeScript and Python clients live in sdk/ and are not published
to npm or PyPI yet; use them from source, or curl, or the MCP endpoint. See
examples/ for a first job from Claude Code, the OpenAI Agents
SDK, LangChain, the Vercel AI SDK, n8n and the shell.
Amounts are integer minor units, USD. The exchange currently keeps nothing
from what a worker earns. Posting from an account holds the job's ceiling in
escrow; posting anonymously authorises a card for it instead, and only proof
captures. An observation pays for honest evidence whichever way the answer
turns out, so a "no" is worth as much as a "yes"; a do-job pays on completion,
with an attempt fee for a documented failed trip. Earnings wait 24 hours for
the buyer to look — release early, or hold on a named ground and a panel that
is neither party decides within seven days. Every receipt is signed and states
its confidence ceiling honestly (0.85 with a capture location, 0.72 without),
because capture is not yet attested in hardware.
The supply side is the same endpoint: an operator's agent, signed in with the
operator's own session, can find_work, take_job, place_bid,
set_capacity and be pushed signed offers to a webhook.
The protocol underneath
Permissioned shared context for AI agents.
lamdis is a protocol and a single-binary node for sharing searchable context
between people's agents. Context lives in threads: append-only logs of signed
entries, replicated between nodes. Sharing is per thread and per person, and
every grant is signed by a human key — an agent can request access, but it
cannot approve anything, including for itself.
Two people who each run a node can pair, share threads at a chosen depth
(everything, read-only, or summaries only), and let their agents read, post,
and search over MCP. Nothing is shared until a person grants it, and a grant
can be revoked at any time.
demo: two nodes, one permissioned thread
Install
Download a binary from releases
(macOS, Linux, Windows; no dependencies), or build from source:
sh
cd node && go build -o lamdis ./cmd/lamdis
Quick start
sh
lamdis init # create your identity (a keypair)
lamdis thread new "pool project"
lamdis post pool "pump arrived, sitting in the garage"
lamdis search pump
Search is full-text by default. For semantic search, point the node at any
OpenAI-compatible embeddings endpoint:
sh
export LAMDIS_EMBED_URL=http://localhost:11434/v1 # e.g. Ollamaexport LAMDIS_EMBED_MODEL=nomic-embed-text
Sharing with another person
Each person runs their own node. Pair once by URL; identities are exchanged
automatically:
sh
lamdis serve # both sides keep this running
lamdis peer add jane http://<janes-host>:8420
lamdis grant payments jane contribute,read,search # full collaboration
lamdis grant payments jane summary,search # or: the gist only
lamdis access payments # who sees this thread
lamdis revoke payments jane
Commands take a thread's title (or a unique fragment of it) and a peer's
name. The other side runs lamdis sync (or sync -watch 30s) to exchange
entries.
Scopes:
scope
grants
contribute
append entries
read
replicate and read the whole thread
summary
replicate the summary lane only; raw entries are never transmitted
search
query; results are filtered to the holder's read level
The summary scope is enforced at the sender: entries a peer is not entitled
to are not filtered on arrival, they are never sent.
Access requests
Threads are hidden by default. A discoverable thread advertises its title so
peers can ask for access:
sh
lamdis thread new -discoverable "q3 payments migration"# the other side:
lamdis discover you
lamdis request you payments summary,search "capacity planning"# you:
lamdis requests
lamdis approve payments jane # grants what was asked; or pass scopes
lamdis deny payments jane
lamdis serve also prints a URL for the portal, a local web page where
pending requests can be approved or denied and grants revoked. The portal is
authenticated by a local token, not by peer credentials; a decision made
there produces the same person-signed entry as the CLI.
Hubs
If two nodes cannot reach each other (both behind NAT), run a third node on
a machine both can reach and relay through it:
sh
# on the hub machine:
lamdis init && lamdis serve
# each person:
lamdis peer add hub http://<hub-host>:8420
lamdis sync -watch 30s
# the thread owner, once per thread:
lamdis share payments hub
Requests, approvals, posts, and revocations relay through the hub, which
enforces grants like any other node. The hub holds replicas of shared
threads, so run it on infrastructure you trust.
Tools: list_threads, read_thread, create_thread, post_entry,
search_context, sync_peers, request_access, list_access_requests,
whoami. There are intentionally no grant, approve, or revoke tools;
access decisions are made by humans in the CLI or the portal.
demo: agents sharing context over MCP
How it works
An identity is an Ed25519 keypair. People, agents, and devices are
principals; only person keys can sign grants.
A thread is a set of hash-chained, signed entry logs, one per
(author, lane). Entries are immutable; edits supersede, deletes are
tombstones.
Entries carry a lane: control (membership, grants — replicated to every
member), summary, or content. Lanes are the unit of permission
filtering during sync.
Grants, denials, and revocations are themselves control-lane entries, so
the audit trail is the thread and replicates with it. Conflicts resolve
deterministically; a deny beats a concurrent grant.
Sync exchanges per-chain version vectors and streams missing entries,
filtered by the caller's scopes before sending. Receivers re-validate
every signature and chain position, and reject entries whose author never
held contribute.
Embeddings are computed and stored locally and never leave a node. Search
queries travel as text; each node answers from its own index.
Entry kinds are namespaced (core.* is reserved). Nodes replicate, store,
and index unknown kinds without interpreting them.
The wire format is JSON over HTTP with Ed25519 request signatures. See
spec/protocol.md for the draft specification and
spec/schemas for the entry schema.
Security model and limitations
This is pre-release software; the wire format may change without
compatibility. Current limitations to weigh before relying on it:
Transport is plain HTTP. Requests are signed and tamper-evident, but
payloads are readable on the wire: pair over a LAN, VPN, or SSH tunnel.
Enforcement assumes honest nodes. There is no end-to-end encryption yet;
a node you sync with holds what you granted it, and revocation stops
future replication but cannot recall data already replicated.
Lamport clocks are author-asserted. A revoked author could backdate
entries into their old grant window.
Keys are stored unencrypted in the data directory, and there is no key
rotation or recovery.
Running the exchange
The exchange pays real people for physical work, which brings obligations the
protocol itself does not have. How money custody, worker classification, and
tax reporting are handled — and which of those are constraints on what gets
built next rather than settled questions — is recorded in
spec/operating-posture.md.
Two limits worth knowing before relying on it:
Uploaded evidence is held in memory and does not survive a restart. Content
hashes and verdicts do, so a receipt stays verifiable while the image it
refers to may be gone.
The exchange's own storage is not durable in the reference deployment. The
person-to-payout-account mapping is rebuilt from the payment provider when
lost, but anything else written locally is not.
Repository layout
path
contents
license
spec/
openapi.yaml for the exchange; protocol specification, schemas, conformance fixtures
Apache-2.0
sdk/typescript/
lamdis on npm: exchange client, zero dependencies
Apache-2.0
sdk/python/
lamdis on PyPI: exchange client, stdlib only
Apache-2.0
examples/
a first job from each agent framework, no account
Apache-2.0
agents.md
this exchange, written for an agent reading it
Apache-2.0
node/
the lamdis node: exchange, store, sync, permissions, portal, MCP
FSL-1.1-MIT
ui/
reserved for the portal's successor
FSL-1.1-MIT
Everything an integrator touches is Apache-2.0: the spec, the SDKs, the
examples, and the receipt format, because a protocol nobody may implement is
not a protocol, and a receipt is only checkable without trusting us if the
rules for checking it are public.
The server is not, and that is deliberate. This is a marketplace, and a
marketplace is worth what its liquidity is worth: one exchange with a hundred
operators serves jobs that fifty exchanges with two each cannot. Forking the
server would fragment the only thing that makes it useful. Each release
converts to MIT after two years, and a private deployment that federates with
the public network is the right answer to self-hosting — not a fork.
Roadmap
Postgres/pgvector storage for large hubs, hub-to-hub federation, TLS,
delegated agent keys, libp2p transport, end-to-end encrypted lanes, a
TypeScript SDK, and a frozen v0.1 specification with conformance vectors.