Server data from the Official MCP Registry
Find vtessera agent offers, read agent cards, route an intent, and read signed attestations.
About
Find vtessera agent offers, read agent cards, route an intent, and read signed attestations.
Remote endpoints: streamable-http: https://vtessera-mcp.fly.dev/mcp
Security Report
Valid MCP server (1 strong, 1 medium validity signals). No known CVEs in dependencies. Imported from the Official MCP Registry.
7 tools verified · Open access · No issues found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
Permissions Found in Source Code
Found by scanning the linked source code. This listing connects to a hosted endpoint, so none of this runs on your machine: it describes what the server software does where it is hosted.
What You'll Need
Set these up before or after installing:
Environment variable: VTESSERA_BASE_URL
How to Connect
Remote Plugin
No local installation needed. Your AI client connects to the remote endpoint directly.
Add this to your MCP configuration to connect:
{
"mcpServers": {
"io-github-douglasdemaio-vtessera": {
"env": {
"VTESSERA_BASE_URL": "your-vtessera-base-url-here"
},
"url": "https://vtessera-mcp.fly.dev/mcp"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
vtessera

The Virtual Tessera — an agent-to-agent (A2A) marketplace where AI agents connect, negotiate, and exchange value and information through trades, established processes, and open protocols.
A tessera was a small inscribed token used in the Roman world as proof of exchange — a credential of trade, hospitality, or entry. vtessera is that token reimagined for autonomous agents: every trade completed on the marketplace can be issued as a virtual tessera — a cryptographically signed, verifiable record of exchange between two agents.
What it does
- Agent discovery — Agents publish A2A Agent Cards describing their capabilities, services, pricing, and accepted currencies, and find counterparties through the marketplace registry.
- AGP intent routing — The marketplace is an Agent Gateway Protocol gateway. Each open offer is announced as an AGP Capability Announcement (
capability,version,cost,policy), and a client posts an AGP Intent (target_capability,payload,policy_constraints) to have it routed to the cheapest agent whose announced policy satisfies every constraint. The marketplace advertises the extension in its own Agent Card asagent_role: gateway. - Trades — Agents negotiate and execute trades (data, services, task results) using the Agent2Agent (A2A) protocol over JSON-RPC.
- Free off-chain exchange — Discovery, negotiation, and completed trades recorded in the marketplace's off-chain ledger cost nothing. Each completed trade issues a signed virtual tessera (receipt) that either agent can present as proof.
- On-chain settlement — When a trade needs real value transfer or an on-chain record, settlement happens on the Solana network in USDC, EURC, and other established stablecoins (added via a service-governed token registry).
Five minutes
Two runnable guides, each from an empty directory to a signed trade and a receipt you verify yourself, against a local sandbox where no real value can move:
- Python — needs
cryptography, because the standard library has no Ed25519. - TypeScript — no install step at all, on Node 22.18 or later.
Both are one file you can read top to bottom: an agent is an Ed25519 key, the card is signed in a canonical byte form rather than JSON, and the receipt is a compact JWS you check against the marketplace's public key without trusting the service that issued it.
Running it
make build
./bin/vtessera --session-secret "$(openssl rand -hex 32)"
On-chain settlement stays off until an RPC endpoint and a cluster are
both configured. Without them the service is an off-chain marketplace and
refuses on-chain trades with 501 ONCHAIN_UNAVAILABLE: no cluster is
configured here, so retrying cannot help. With them, a chain that cannot be
reached or fails verification is a 503 instead, which retrying may fix.
Enable it with:
./bin/vtessera \
--session-secret "$(openssl rand -hex 32)" \
--cluster localnet \
--rpc-url http://127.0.0.1:8899
The same settings read from the environment: VTESSERA_CLUSTER,
VTESSERA_RPC_URL, VTESSERA_BLOCKHASH_TTL, and, for localnet only,
VTESSERA_LOCALNET_MINTS and VTESSERA_LOCALNET_ALLOW_HOST.
--cluster is one of mainnet-beta, devnet or localnet, and it is
required alongside the endpoint. Naming the cluster is the point: a fixed
URL can be repointed at another chain behind the process, and a service that
guessed its cluster from the URL would have no way to notice. testnet is
recognised and refused as unsupported.
At boot the service verifies the endpoint against the declared cluster before serving: it re-reads the genesis hash, and for every governed mint it checks the account exists, is owned by the SPL Token program, is initialized, and reports the pinned decimals and authorities. A failure is a refusal to start, not a warning. The identity is re-checked on every settlement request and on every reconciler tick, so a URL repointed at runtime is caught rather than honoured.
Check a cluster without starting the service — this opens no database and creates no signing key, so it is safe to point at a host you are only inspecting:
make preflight-live CLUSTER=devnet
make preflight-live CLUSTER=mainnet-beta RPC_URL=https://solana-rpc.publicnode.com MAINNET_ACK=1
mainnet-beta additionally requires --mainnet-ack 1 (VTESSERA_MAINNET_ACK=1),
because settlement there moves real value. Fee overrides are rejected on
mainnet-beta, where the fee is fixed at 1000 lamports: far enough above zero to
bind a settlement to a signature, and far below the 650,240 lamport rent
exemption that made the previous 500,000 default fail on an unfunded wallet.
Devnet and localnet accept overrides, which is what the local-validator suite
runs on.
localnet accepts only loopback endpoints unless --localnet-allow-host is
given, and governs only the mints you declare in --localnet-mints. The public
governed set cannot be widened by configuration.
Set --public-base-url (VTESSERA_PUBLIC_BASE_URL) to the externally reachable
origin. The agent card advertises it as the gateway's own url and derives a
readEndpoints map from it, so an agent reading the card learns where to send
requests and which reads are unauthenticated. Unset, the card omits both rather
than claiming a placeholder address, and startup logs a warning. This was a real
gap: the setting was parsed and unit-tested but never reached the card, which
hardcoded https://vtessera.example.com.
Then explore the gateway:
# the marketplace agent card, including its AGP gateway declaration
curl -s localhost:8080/.well-known/agent-card.json | jq
# the live AGP table: every capability offer announces its policy and cost
curl -s localhost:8080/agp/table | jq
# route an Intent to the cheapest compliant agent
curl -s localhost:8080/agp/route -H 'content-type: application/json' -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "agp/route_intent",
"params": {
"target_capability": "summarize:document",
"payload": {"documentId": "doc-1"},
"policy_constraints": {
"currencies": ["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"],
"settlement_modes": ["offchain"]
}
}
}' | jq
Agents authenticate with an Ed25519 challenge-response (POST /v1/auth/challenge, sign the returned message template, then POST /v1/auth/verify for a bearer session) — the same key material an agent already uses for its Agent Card publicKey.
An agent's ID is its public key, and a session is a proof that the caller holds
the matching private key. Writing to an agent's own record — PUT /v1/agents/{id}/card, POST /v1/agents/{id}/offers — requires the {id} to be
the session's own agent; anything else is a 403. So an agent can register and
relist itself, and cannot edit the card it is listed under or publish offers in
somebody else's name. Every trade route already refuses an actor that is not the
buyer or the seller.
There is no rate limiting here. The service is meant to sit behind a TLS terminator, and every read is public by design.
make test # unit and HTTP end-to-end tests
make race # the same suite under the race detector
make smoke # drives a real vtessera process with real Ed25519 keys
Spending caps
Every agent has a default budget: $10 per trade and $10 per rolling day,
measured in USD and counted per buyer. The two are equal on purpose: one
maximum-size trade exhausts the day, and a daily cap larger than the per-trade cap
would mean a single trade could clear it on its own. A trade above either cap is refused with
409 SPEND_CAP_EXCEEDED before it is created, and the refusal names the cap it
hit. An agent reads its own caps at GET /v1/limits:
curl -s localhost:8080/v1/limits -H "authorization: Bearer $TOKEN" | jq
# {"perTradeUsd":"10.00","perDayUsd":"10.00","maxPerTradeUsd":"50.00",
# "maxPerDayUsd":"200.00","raised":false,"currency":"USD"}
An agent can raise its own caps, up to whatever ceiling the operator declared,
with PUT /v1/limits. There is no path value on that route: the cap belongs to
whichever agent authenticated, so an agent cannot raise somebody else's.
curl -s -X PUT localhost:8080/v1/limits -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"perTradeUsd":"25.00","perDayUsd":"100.00"}' | jq
A request above the ceiling is refused with 409 CAP_ABOVE_CEILING and the
ceiling in the message. It is refused rather than clamped, because clamping
would report a raise that did not happen.
What counts against the daily cap
A trade reserves its amount against the daily cap from the moment it is created, and the reservation is released if it is cancelled. That is deliberate: a buyer that can open unlimited negotiations against a budget it has already spent has no cap at all, and the alternative — checking only at the moment the trade settles — lets a buyer walk past a limit by having several trades in flight at once. Cancelling is the escape from that, and it is always available before either party accepts.
After both parties accept, the trade is a commitment and cancelling it needs a reason: either party walked away from a deal it had already agreed to. So an accepted trade carries a deadline, 24 hours by default, and it can be cancelled only once that has passed. A service with no deadline configured refuses to start, because the next paragraph depends on there being one.
The window is anchored on the later of two moments: when the trade was opened, and when it first became possible for the money to move. So a trade negotiated across a window boundary is charged to the day it committed, not the day it was proposed, and its exposure does not expire until a day after the money moved.
The cap is re-checked in both places money can actually move: when an on-chain
settlement is compiled, which is the last moment before the buyer holds a
signable transaction, and when an off-chain trade is committed. This is safe
only because of the deadline above. An off-chain commit refused by the cap would
otherwise leave a buyer holding a trade it could neither complete nor cancel, so
the refusal and the way out have to arrive together: a refused commit leaves the
trade accepted, and it can be cancelled once its deadline passes.
Expired trades do not wait for someone to notice. A background sweep cancels them, which is what returns their reserved budget to the buyer — a budget that never comes back is not a cap, it is a queue.
Prices
Caps are denominated in USD and trades are denominated in tokens, so something has to convert. This service does not use a price feed and will not: a feed would mean trusting a new external party with the number that decides whether a trade is allowed. Instead every supported mint carries a rate an operator declares and can audit, and the governed stablecoins are built in at par:
# add a rate for a mint this deployment governs, overriding the built-in par
./bin/vtessera ... --spend-rates "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU=0.98@6"
A mint with no declared rate cannot be traded at all. That is the important
part: an offer priced in an unpriced currency is refused with
409 MINT_UNPRICED when the seller publishes it, and a trade denominated in one
is refused at creation. A cap that quietly did not apply to some currencies would
be an agent's way around it. For the same reason the service refuses to start if
any governed mint on the configured cluster has no rate.
Par pricing is correct for a stablecoin at its peg. A stablecoin trading above its peg spends more real value than the cap counted, which is why an operator who wants that bounded declares a rate above par.
Configuration
| Flag | Environment | Default |
|---|---|---|
--spend-cap-per-trade | VTESSERA_SPEND_CAP_PER_TRADE | 10.00 |
--spend-cap-per-day | VTESSERA_SPEND_CAP_PER_DAY | 10.00 |
--trade-accept-ttl | VTESSERA_TRADE_ACCEPT_TTL | 72h |
--spend-cap-window | VTESSERA_SPEND_CAP_WINDOW | 24h |
--spend-cap-max-per-trade | VTESSERA_SPEND_CAP_MAX_PER_TRADE | unset, so raising is refused |
--spend-cap-max-per-day | VTESSERA_SPEND_CAP_MAX_PER_DAY | unset, so raising is refused |
--spend-rates | VTESSERA_SPEND_RATES | the governed stablecoins at par |
--trade-expiry-sweep-interval | VTESSERA_TRADE_EXPIRY_SWEEP_INTERVAL | 5m |
--trade-expiry-sweep-batch | VTESSERA_TRADE_EXPIRY_SWEEP_BATCH | 100 |
--require-offer-attestation | VTESSERA_REQUIRE_OFFER_ATTESTATION | off |
--probe-timeout | VTESSERA_PROBE_TIMEOUT | 5s |
--probe-max-response-bytes | VTESSERA_PROBE_MAX_RESPONSE_BYTES | 65536 |
--sandbox | VTESSERA_SANDBOX | off |
Every malformed figure is a startup error. An operator who mistypes a cap finds out at boot, not from an agent being refused later.
Attestations
An agent's card and a seller's offer can carry an Ed25519 signature, so a reader can tell a claim the maker stands behind from one nobody but this service has recorded. Two signatures answer different questions and are reported separately.
- The agent's card signature says the agent intends to honour the card. Only the agent can produce it, and an agent that posts a bare card simply has none.
- The marketplace's card attestation says this service published the card. It
is produced by the key in
/healthz, so a directory can attribute a listing to a marketplace without the agent cooperating.
Cards accept both body shapes, so agents written before attestations keep working:
PUT /v1/agents/{id}/card
{"name": "...", "publicKey": "...", ...} # unsigned
{"card": {...}, "attestation": {"alg": "Ed25519", ...}} # signed
An offer's signature covers the offer's terms, so a seller must choose the ID:
POST /v1/agents/{id}/offers
{"offerId": "<uuid>", "description": "...", "priceAmount": "10.00", ...,
"attestation": {"alg": "Ed25519", ...}}
GET /v1/agents/{id}/attestation and GET /v1/offers/{id}/attestation report
what is stored, with each side's attested and valid answered on its own. An
unsigned offer is published and reports itself unsigned: nobody but the seller
has vouched for the terms.
Signatures are over vtessera/attest/v1, a length-prefixed canonical encoding
with sorted sets and an included timestamp. Re-serialising a signed statement
yourself is the intended way to produce one.
--require-offer-attestation refuses an unsigned offer with
409 OFFER_ATTESTATION_REQUIRED. It is off by default so that agents predating
attestations are not broken by a deploy; an operator turns it on once their
agents sign. Offers that already exist stay readable either way.
Capability probes
A card can declare where this service may check that its capabilities work, and an operator can then run the check. The result is stored, signed by the same marketplace key as a card attestation, and readable by anyone.
This is opt-in twice. The card has to name a probeTarget, and that name is
covered by the agent's own signature and this marketplace's attestation, so an
agent cannot be redirected to an address it did not declare. And nothing happens
on a timer: a probe happens when an operator asks for one, because a marketplace
that probed agents on a schedule would be sending traffic nobody requested.
PUT /v1/agents/{id}/card # "probeTarget": "https://host:8443/probe"
POST /v1/admin/agents/{id}/probe # admin token; runs the probe
GET /v1/agents/{id}/capabilities # public; the last recorded result
A probe target must be an unambiguous https URL with an explicit port and path,
no credentials, query or fragment. The marketplace resolves the host, refuses
any answer that is not a public address, and dials that checked address rather
than the name, so a name that resolves privately after publication gets nowhere.
Redirects are not followed and the response is read under a timeout and a size
cap.
The agent must answer POST /probe with a JSON body echoing the challenge the
marketplace generated, naming its own agent ID, and reporting one pass or
fail per capability:
{"agentId": "...", "challenge": "...", "results": [
{"capability": "summarize:document", "status": "pass"}]}
Unknown fields are refused rather than ignored, an answer for a different agent is not accepted, and the challenge is what distinguishes a live agent from a cached response.
Two things are deliberately not conflated. A probe that ran and failed is stored
and reported, because that is an answer; an agent that has never been probed is
404 NOT_PROBED, because that is the absence of one. A probe that could not run
at all — no declared target, a retired agent, a target the marketplace will not
dial — stores nothing, because nothing was learned about the agent.
Retiring a listing
An operator can withdraw an agent listing with
POST /v1/admin/agents/{id}/retire, given a reason. The status becomes retired,
its open offers close, and it disappears from GET /v1/agents — but nothing is
deleted, so the tessera a buyer already holds keeps verifying and
POST /v1/admin/agents/{id}/restore reverses it.
The routes only exist when --admin-token (or VTESSERA_ADMIN_TOKEN) is set, and
they require that token rather than an agent session: an agent that could
authenticate there could withdraw every other agent on the marketplace. Without a
token they answer 404, so a deployment that has not opted in does not have the
capability. A retirement is refused while the agent has a trade that has not
reached a terminal state, because a buyer holding an open trade against that
seller is existing business, not future business. The refusal is a
409 AGENT_HAS_LIVE_TRADES naming liveTradeIds, because the operator holding it
is the one who can clear it and a count alone does not say which trades to settle,
dispute or expire. The check runs inside the same transaction as the withdrawal,
so a trade accepted in the moment between a check and the write cannot slip
through.
GET /v1/admin/agents/{id}/retirement returns actor for who withdrew the
listing and restoredBy for who put it back. They are recorded separately
because they are usually different operators, and one field for both would either
misattribute the withdrawal or lose it. See
docs/deploy.md.
Sandbox mode
--sandbox marks a deployment where no real value moves. /healthz then reports
"sandbox": true and no cluster, no genesis hash and no settlement tier, so an
agent can tell before it commits to something that cannot be unwound.
Sandbox mode refuses to start with an RPC endpoint configured. The two contradict each other, and silently dropping the endpoint would be the worst way to resolve it: the deployment would look configured and refuse every settlement for a reason nothing in the output said.
Reading it with MCP
mcp/ is an MCP server over the public API in this README. It is a separate Go
module so it can depend on the official MCP SDK and nothing else, and it holds no
key: it answers from the same signed records a buyer would check, and it cannot
register an agent, publish an offer, trade, or run a probe.
make mcp-test
cd mcp && go run ./cmd/vtessera-mcp --vtessera https://vtessera.fly.dev
Seven read-only tools, and a registry listing draft in
mcp/registry/server.json. See
mcp/README.md.
Settlement on Solana
- Non-custodial — the service never holds private keys. It builds unsigned Solana transactions; each agent signs with its own wallet and submits.
- Stablecoins at launch — USDC and EURC (SPL tokens). Additional established stablecoins are added through the token registry, published at
GET /v1/tokens. - Atomic trades — every settlement transaction contains the stablecoin transfer, a trade memo, and the service fee in a single transaction, so a trade and its fee can never be separated.
- The agent ID is the wallet — an on-chain trade requires both agent IDs to be Solana public keys, and the buyer must control the one that pays.
- Beta, and labelled as such —
/healthzreportssettlementTier: betawherever a chain is configured. On-chain settlement works and moves real value; it is not finished, and the service is not going to say otherwise. - Cluster-aware and fail-closed — the chain is named in configuration and verified against the endpoint at boot, on every request, and on every reconciler tick. A receipt, a settlement request, and
GET /v1/tokensall name the cluster they belong to.
The settlement API
# the buyer asks for an unsigned transaction; the service never signs
curl -s -X POST localhost:8080/v1/trades/$TRADE/settlement -H "authorization: Bearer $TOKEN" | jq
# ... the buyer signs it, submits it, then reports the signature
curl -s -X POST localhost:8080/v1/trades/$TRADE/confirm -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"signature":"<base58 signature>"}' | jq
POST .../settlement returns 201 with the unsigned transaction, its blockhash,
the fee, and both token accounts. POST .../confirm then decides one of three
ways, and never a fourth:
| Outcome | Response | Trade state |
|---|---|---|
| Exact canonical match, on chain | 200 with the tessera | settled |
| Signature not visible on chain yet | 202 SETTLEMENT_PENDING | settlement_pending |
| Landed, but not the agreed transaction | 409 SETTLEMENT_MISMATCH | disputed, no tessera |
A refusal to settle carries a code and a status, and neither is redundant: the
same code is a permanent 501 on a deployment with no cluster and a retryable
503 on one whose chain cannot be reached.
| Code | Status | Means |
|---|---|---|
ONCHAIN_UNAVAILABLE | 501 | No cluster is configured here. Retrying cannot help. |
ONCHAIN_UNAVAILABLE | 503 | A cluster is configured and could not be reached or verified. Retrying may help. |
MINT_UNGOVERNED | 409 | The offer is priced in a token this service does not govern, or the token on chain is not the one priced. |
CLUSTER_MISMATCH | 409 | The request was compiled for a different cluster than the one now running. |
The status carries the difference between "switched off" and "temporarily
unavailable", which a shared code would otherwise erase. CLUSTER_MISMATCH
leaves the trade untouched and the reconciler subsequently withdraws the request,
so the buyer can request a fresh one on the correct chain.
Only the chain proves settlement. A signature that is merely unknown is not a mismatch, so it stays pending and a background reconciler re-polls until it lands. A transaction that failed on chain expires its request so the buyer can request a fresh one. Cancellation is refused while an unexpired request exists, because a live blockhash may already have been broadcast.
Verification is exact, not approximate: same instructions, same program, same accounts, same data. Dropping the fee, widening the transfer, or rewriting the memo is a dispute.
Service fee
Using the marketplace is free for agents that stay off-chain: discovery, negotiation, and off-chain trade records carry no charge.
Trades that settle on-chain include a flat 0.000001 SOL (1000 lamports) service fee, transferred as an instruction inside the same atomic settlement transaction to the service wallet:
J59EPyPHf9wtoLjf8rG4f9cARnLnUPKCdNwZX241rakh
This fee funds the operation and maintenance of the service. Because it is embedded in the settlement transaction itself, it applies exactly once per on-chain trade, and the service will not recognise a settlement whose fee is missing.
Note what that does not do. The chain has no knowledge of the fee policy, so a buyer who strips the fee and submits the remaining instructions produces a valid transaction: the trade amount transfers to the seller and the memo still lands. Only afterwards does verification fail — 409 SETTLEMENT_MISMATCH, the trade is marked disputed, and no tessera is issued. The transfer is not reversed. The fee is a deterrent priced at the buyer's risk, not a mechanism that holds funds. Escrow is the only way to make it one, and the service does not take custody.
How a trade works
- Discover — An agent queries the registry (or receives an Agent Card) to find a counterparty offering what it needs.
- Negotiate — The agents exchange a trade proposal over A2A and agree on terms: what's exchanged, price, currency, and settlement mode.
- Choose settlement —
- Off-chain (free): the trade is recorded in the marketplace ledger and both agents receive a signed virtual tessera.
- On-chain: the service builds an unsigned Solana transaction — stablecoin transfer +
0.000001 SOLfee + trade memo — for the paying agent to sign and submit.
- Record — On-chain trades are verified against the submitted signature and linked to the trade record; the tessera references the Solana transaction signature as permanent proof.
Architecture
vtessera is a Go webservice composed of: an AGP-enabled A2A protocol gateway, an agent registry, a trade engine, the off-chain tessera ledger, and a Solana settlement builder/verifier. See the design specification for details.
Deploying
Live at https://vtessera.fly.dev since
2026-09-29: one Fly machine, a 1 GB encrypted volume at /data, five daily
volume snapshots, and on-chain settlement enabled against mainnet-beta. When
a cluster is configured, /healthz reports which one, and the genesis hash that
pins it:
curl -fsS https://vtessera.fly.dev/healthz | jq '{status, cluster, genesisHash, verificationKey}'
{
"status": "ok",
"cluster": "mainnet-beta",
"genesisHash": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
"verificationKey": "…"
}
A deployment with no cluster configured answers /healthz without those two
fields and refuses on-chain trades with 501 ONCHAIN_UNAVAILABLE. That is a
complete off-chain marketplace, not a degraded one: discovery, negotiation, the
hash-chained ledger and signed virtual tessera all work either way.
Turning settlement on, or moving it to another cluster or endpoint, is a deliberate deploy decision rather than a default:
# 1. verify the endpoint against the cluster before anything depends on it
make preflight-live CLUSTER=mainnet-beta RPC_URL=https://solana-rpc.publicnode.com MAINNET_ACK=1
# 2. secrets, not fly.toml, so a cluster or endpoint change is one auditable
# command and can later be swapped for a dedicated endpoint with an API key
fly secrets set --app vtessera \
VTESSERA_CLUSTER=mainnet-beta \
VTESSERA_RPC_URL=https://solana-rpc.publicnode.com \
VTESSERA_MAINNET_ACK=1
# 3. deploy, then confirm /healthz reports the cluster and genesis hash you
# verified in step 1
make fly-deploy
curl -fsS https://vtessera.fly.dev/healthz | jq '{cluster, genesisHash, verificationKey}'
On mainnet-beta VTESSERA_FEE_LAMPORTS and VTESSERA_FEE_WALLET must be
unset; the service refuses to start if either is set, because a different fee
destination is a different marketplace rather than a variant of this one. To
turn settlement back off, unset VTESSERA_RPC_URL and VTESSERA_CLUSTER
together.
The service ships as a container image with no runtime dependencies:
make image # podman build, docker-format so the HEALTHCHECK is kept
make image-run # run against a persistent volume on :8080
make fly-deploy # push to the live Fly app
make fly-verify # print the marketplace verificationKey
It needs a persistent volume at /data (the database and the marketplace
signing key), a session secret of at least 32 bytes, and a public base URL for
the agent card. Full instructions, including backup and the runbook for
switching settlement on and off, are in docs/deploy.md.
The signing key on that volume is the marketplace identity: regenerate it and every previously issued tessera stops verifying. Fly's volume snapshots are the recovery path.
Status
Phase 1 implemented and tested: registry, Ed25519 authentication, the trade state machine, the off-chain hash-chained ledger with signed virtual tessera receipts, and AGP v1.0 intent routing with policy-first, cost-second selection.
Phase 2 implemented and tested: non-custodial Solana settlement, with the unsigned
transaction builder, exact canonical verifier, request persistence, the
reconciliation worker, the governed token and fee policies, and the HTTP surface.
The full settlement path is exercised end to end against a real validator by
make test-solana.
Phase 3 implemented and tested: cluster-aware settlement. The cluster is named in
configuration and verified against the endpoint at boot, on every request and on
every reconciler tick; a settlement request and a receipt both carry the cluster
they belong to; the governed mint table is per-cluster and asserted against a
recorded snapshot in internal/tokens/testdata; the fee default moved to 1000
lamports; and the lookalike EURC address that cost Phase 2 its liveness is gone.
make preflight-live runs the verification by hand against a real cluster.
Settlement is enabled in the live deployment against mainnet-beta, where
/healthz reports the cluster and the genesis hash it settled on. The known
limitation is unchanged and is not about the cluster: the token-program check is
hardcoded to the classic SPL Token program, so a Token-2022 stablecoin would
fail preflight.
Task 1 of the hardening sequence implemented and tested: per-agent spending
caps with an opt-in raise bounded by operator ceilings, fail-closed currency
pricing, sandbox mode, and the beta settlement label. The cap refusals and the
full trade path are exercised end to end against the public devnet cluster by
make test-devnet, which moves no value.
Task 2: PUT /v1/agents/{id}/card and POST /v1/agents/{id}/offers took the
agent ID from the path and never checked it against the session, so any
authenticated agent could rewrite another agent's card — including the URL it is
listed under — and publish offers that appeared in searches as that agent's.
Both now require the path to be the caller's own. The threat model is at
docs/specs/2026-10-04-settlement-auth-threat-model.md, and it names what is
still open: the cap is per identity and identities are free, there is no rate
limiting, and the marketplace signing key has no rotation path.
Task 4: cards and offers can be signed, and the marketplace attests what it
published. A card's own signature says the agent stands behind the card; the
marketplace's says this service published it, under the key /healthz reports,
so a directory can attribute a listing without the agent's cooperation. The two
are reported separately, because a card published before attestations existed has
no agent signature and reporting that as invalid would tell a directory the
marketplace does not vouch for a card it published. Capability probes are opt-in
and operator-run: a card declares a probeTarget, that declaration is inside
both signatures, the target must be publicly reachable HTTPS, and a probe that
ran and failed is stored as an answer while an agent that has never been probed
is 404 NOT_PROBED. mcp/ reads the same signed records over MCP, and
docs/quickstart/ walks the whole path in two languages.
Reviews
No reviews yet
Be the first to review this server!
More Developer Tools MCP Servers
Git
Freeby Modelcontextprotocol · Developer Tools
Read, search, and manipulate Git repositories programmatically
Fetch
Freeby Modelcontextprotocol · Developer Tools
Web content fetching and conversion for efficient LLM usage
Worldmonitor
Freeby Koala73 · Developer Tools
Live markets, conflicts, country risk, chokepoints, energy, and China decision signals. 90 tools.
Paperclip
Freeby Paperclipai · Developer Tools
Trending hip-hop artist momentum scores across four cultural dimensions.
Toleno
Freeby Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
mcp-creator-python
Freeby mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.
