Back to Browse

Vtessera MCP Server

Developer ToolsLow Risk10.0MCP RegistryRemote
Free

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

10.0
Low Risk10.0Low Risk

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.

HTTP Network Access

Connects to external APIs or services over the internet.

What You'll Need

Set these up before or after installing:

Base URL of the vtessera marketplace to read. Defaults to http://localhost:8080.Optional

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 GitHub

From the project's GitHub README.

vtessera

vtessera logo

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 as agent_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

FlagEnvironmentDefault
--spend-cap-per-tradeVTESSERA_SPEND_CAP_PER_TRADE10.00
--spend-cap-per-dayVTESSERA_SPEND_CAP_PER_DAY10.00
--trade-accept-ttlVTESSERA_TRADE_ACCEPT_TTL72h
--spend-cap-windowVTESSERA_SPEND_CAP_WINDOW24h
--spend-cap-max-per-tradeVTESSERA_SPEND_CAP_MAX_PER_TRADEunset, so raising is refused
--spend-cap-max-per-dayVTESSERA_SPEND_CAP_MAX_PER_DAYunset, so raising is refused
--spend-ratesVTESSERA_SPEND_RATESthe governed stablecoins at par
--trade-expiry-sweep-intervalVTESSERA_TRADE_EXPIRY_SWEEP_INTERVAL5m
--trade-expiry-sweep-batchVTESSERA_TRADE_EXPIRY_SWEEP_BATCH100
--require-offer-attestationVTESSERA_REQUIRE_OFFER_ATTESTATIONoff
--probe-timeoutVTESSERA_PROBE_TIMEOUT5s
--probe-max-response-bytesVTESSERA_PROBE_MAX_RESPONSE_BYTES65536
--sandboxVTESSERA_SANDBOXoff

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 — /healthz reports settlementTier: beta wherever 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/tokens all 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:

OutcomeResponseTrade state
Exact canonical match, on chain200 with the tesserasettled
Signature not visible on chain yet202 SETTLEMENT_PENDINGsettlement_pending
Landed, but not the agreed transaction409 SETTLEMENT_MISMATCHdisputed, 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.

CodeStatusMeans
ONCHAIN_UNAVAILABLE501No cluster is configured here. Retrying cannot help.
ONCHAIN_UNAVAILABLE503A cluster is configured and could not be reached or verified. Retrying may help.
MINT_UNGOVERNED409The offer is priced in a token this service does not govern, or the token on chain is not the one priced.
CLUSTER_MISMATCH409The 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

  1. Discover — An agent queries the registry (or receives an Agent Card) to find a counterparty offering what it needs.
  2. Negotiate — The agents exchange a trade proposal over A2A and agree on terms: what's exchanged, price, currency, and settlement mode.
  3. 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 SOL fee + trade memo — for the paying agent to sign and submit.
  4. 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.

Logos: logo.svg (source), logo.png (rendered).

Reviews

No reviews yet

Be the first to review this server!