Skip to main content
Glama
README.md
# VectorAgent

An icon store where the buyer is an agent and the payment is the license.

An agent finds the tools over MCP, searches for free, gets an HTTP `402` when it
asks for a paid icon, signs a sub-cent USDC transfer on Stellar testnet, and
gets the SVG back with the transaction that bought it stamped into the file.

Testnet only. Every number below was measured rather than estimated.

## Check it before reading the rest

None of this needs to be taken on trust. One real settlement:

| | |
| --- | --- |
| Payment | [`ec6405b8…a6409751`](https://stellar.expert/explorer/testnet/tx/ec6405b887dede5453ef4ac663760791e657f2cea1bfd5e4727b7f62a6409751) - 0.0180 USDC, agent wallet to vault |
| Receipt | [`f60c428f…9baadda8`](https://stellar.expert/explorer/testnet/tx/f60c428f17cad889bfeffed7d3b2361ee0b30dbbefcf4fa8aa87f29d9baadda8) - a `manage_data` entry named `va:ec6405b887dede5453ef4ac663760791` |
| What it bought | 7 icons, in a 524-byte manifest whose sha256 is `5a759b7d…a643b843`, which is the value that entry holds |

The entry's name is the first 32 characters of the payment hash, so the link
between the money and the list is public too. The
[settlements page](https://vectoragent.telby.io/ledger) refetches every manifest
and re-hashes it in your browser: anything that does not match the chain is
dropped rather than shown, so the page cannot vouch for a list the ledger has
not already committed to. Every settlement there was paid by this project's own
wallet, some from the demo on the home page and some from an agent calling the
MCP tools directly.

[The live demo](https://vectoragent.telby.io/) runs an actual purchase when you
press the button. It is not a recording.

## What this is

A proof of one narrow thing: an agent can buy a digital asset on its own, with
no human approving the purchase, and the payment can be the provenance record
rather than a license file someone has to keep believing in.

Three standards stacked, none of them invented here. MCP is how agents find
tools, x402 is how a server charges for a request, and Stellar settles cheaply
enough that writing the receipt costs less than the icon.

The findings, including what happened when four agents met it cold and only two
of them bought anything, are written up
[here](https://pbaunach.github.io/portfolio/case-studies/vector-agent/). This
README is the implementation.

## Quickstart

```bash
npm install
cp .env.example .env
npm run setup-wallets   # AGENT + VAULT keypairs, funded with XLM, USDC trustlines opened
```

Two things you have to bring yourself:

- **A facilitator API key.** Set `FACILITATOR_API_KEY` in `.env`. The default
  `FACILITATOR_URL` is OpenZeppelin's hosted testnet facilitator.
- **Testnet USDC for the agent wallet.** The Circle faucet is a web form with no
  public API, so this step is manual: open <https://faucet.circle.com>, network
  **Stellar Testnet**, token **USDC**, paste your `AGENT_PUBLIC_KEY`. 20 USDC per
  request, refillable every two hours.

Then run the payment loop end to end:

```bash
npm run build:edge                # copy src/ and assets/ into the function
npm run dev:edge                  # terminal 1: the x402 resource server on :4021
npm run test:flow shopping-cart   # terminal 2: one paid fetch over plain HTTP
```

And the static site, if you want the settlements page and the demo:

```bash
npm run build:previews   # 1,550 watermarked PNGs into site/dist/preview
npm run build:site       # settlements page, library browser
npm run build:demo       # the home page
npm run serve:site       # http://localhost:4180
```

`PREVIEW_BASE` already defaults to that address, so the previews in a `402`
challenge resolve against your own copy rather than anyone else's.

## The asset library

1,550 original icons across 30 categories, built from compact specs in
`assets/specs/` by `npm run build:assets`. Every icon is tokenized the same
way: each part carries a `data-slot` marker and paints through a CSS custom
property chain, so one file serves light, dark, mono and duotone without
variants.

```svg
stroke="var(--tby-accent-stroke, var(--tby-accent, var(--tok-color-secondary, #1a1f2b)))"
```

| Tier | Count | Price | What it is |
| --- | --- | --- | --- |
| free | 1,000 | $0.000 | common UI primitives, 20 categories |
| standard | 500 | $0.002 | domain-specific (dev-infra, fintech, healthcare, …) |
| premium | 50 | $0.010 | animated variants of standard icons |

`npm run preview:assets` writes `assets/qa/preview.html`, a self-contained
page with every icon inlined and toggles for theme, palette and a per-region
accent override. `npm run qa:assets` writes per-category PNG contact sheets.

These are a test set, published under the same license as the code. They exist
to make the payment flow real, not to be a product.

## Pricing

Price is a property of the icon's tier, not of the route. The x402 route
config takes a function, so one endpoint quotes three different prices:

```ts
price: (ctx) => getIcon(iconIdFromPath(ctx.path))?.price ?? TIERS.standard.price
```

Three cases resolve in order, and the first two never mint a challenge:

| Request | Result |
| --- | --- |
| unknown id | `404` ahead of the payment middleware, so a typo is never billable |
| free icon | `200` with the SVG, no 402 round trip at all |
| paid icon | `402` + preview, then the SVG once the transfer settles |

The 402 is not a dead end. Its **body** carries the free assessment payload
(tier, price, tags, and a watermarked 96px preview URL) while the
machine-readable requirements ride in the `PAYMENT-REQUIRED` **header**, so
the two never collide. `src/preview.ts` renders the watermark; it is
deliberately low-resolution and overprinted, enough to judge the icon and
useless as the asset.

## Latency, and why baskets exist

A settlement costs a Stellar ledger close, and Stellar testnet closes a ledger
every **exactly 5.00 seconds**. Measured end to end, a single paid fetch is
~8-10s, and splitting it around the ledger boundary shows where it goes:

| | Time |
| --- | --- |
| Request to ledger close | ~4.9-5.6s (protocol floor) |
| Ledger close to response | ~1.8-4.8s (facilitator confirm, pure overhead) |
| Free icon | 51 ms |
| 402 challenge only | 4 ms |

So roughly half the wait happens *after* the money has already moved, and the
other half cannot be removed while we wait for settlement at all. That makes
per-icon fetching a dead end for an agent building a screen: twenty icons one
at a time is about three minutes.

`GET /bundle/:ids` buys a basket in one settlement, which is the cheapest fix
available and changes no trust assumptions. Measured:

```
5 icons sequentially   39.00s   (7.80s each)
5 icons as one basket   7.50s   5.2x faster, same $0.010 spent
20 icons as one basket  8.06s   versus roughly 180s one at a time
```

Latency is per settlement, not per icon, so a twenty-icon basket costs about
the same wall time as buying one. The `fetch_vector_icons` MCP tool says so in
its description, so a calling agent has a reason to prefer it.

The basket keeps the same three-case ordering as a single fetch: an unknown id
rejects the whole basket **before** the middleware so nothing is billed, an
all-free basket never sees a challenge, and free icons ride along inside a paid
basket at no cost so the caller never has to sort them. Limit is 40.

Two things this does **not** fix, and they are the real roadmap: the facilitator
overhead above (PayAI took x402 on Base from 1.7s to
[750ms](https://blog.payai.network/sub-second-x402-settlement-on-base-with-flashblocks/)
by chasing exactly this), and genuine first-contact one-off requests, which no
deployed x402 pattern currently solves without pre-funding. Releasing an asset
on verification alone is tempting and is what we first reached for, but it is a
documented vulnerability class rather than a pattern: a production deployment
measured [86.95% leakage](https://arxiv.org/pdf/2605.30998) under burst.

## The license attribute

A delivered asset carries a pointer to the settlement that paid for it, so
provenance survives without us:

```svg
<svg data-va-asset="vector-db"
     data-va-license="standard"
     data-va-tx="0a0155746e73…ccfb"
     data-va-network="stellar:testnet" …>
```

Free assets are stamped too, with `data-va-license="free"` and no citation.
The claim is that an audit finds zero *unattributed* assets, and "free"
answers "where did this come from" while a missing attribute does not.

`data-va-network` is load-bearing: a bare hash is meaningless without knowing
which ledger to resolve it against, and resolving it is the whole point.

**The MCP server stamps this, not the resource server.** The Stellar scheme's
default payment flow is `authorization`, which settles *after* the route
handler produces its body, so the server cannot know the transaction hash
when it builds the SVG. The MCP server can: it holds the wallet, it made the
payment, and it gets the settlement receipt alongside the asset. The party
that paid writes the pointer to its own payment into its own copy.

### Auditing

```bash
npx tsx scripts/audit-licenses.ts ./some-app/src
```

Walks a tree, finds every stamped asset (including SVGs inlined into JSX),
and resolves each citation against public Horizon. It never calls the
VectorAgent registry, which is the point: the audit still works if the
company is gone. `verify_vector_license` is the same check as an MCP tool.

### What this does and does not prove

Resolving a citation proves a settled transfer of a given amount reached a
given account at a given time, and the verifier additionally checks that the
amount matches the asset's tier price. It does **not**, on its own, prove
that transfer bought *this* icon, because the asset id is not carried inside
the transaction: that link still comes from the registry's records. Closing
it needs the asset id in a Stellar memo or an asymmetric signature.

## Testing

```bash
npm run dev:edge       # terminal 1
npm run test:pricing   # terminal 2 - HTTP + ledger
npm run test:mcp       # terminal 2 - the MCP tool layer
npm run test:bundle    # terminal 2 - batching, measured against sequential
npm run test:manifest  # terminal 2 - canonical bytes and their digest
npm run test:attest    # terminal 2 - the on-chain receipt round trip
```

`test:pricing` asserts an unknown id is never billable, a free icon settles
nothing, and standard and premium are quoted **and** settled at $0.002 and
$0.010, checking the vault's balance on Horizon rather than trusting the
server. `test:mcp` spawns the MCP server over stdio and checks that an agent
sees the right tools, that search and preview cost nothing, and that only
`fetch_vector_icon` can spend.

Every one of these spends real testnet USDC from the agent wallet, which is
the point: they fail if the money does not actually move.

## Using it from Claude Code

`.mcp.json` at the project root registers the MCP server:

```json
{
  "mcpServers": {
    "vectoragent-poc": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "src/mcp-server.ts"]
    }
  }
}
```

1. Start (or restart) a Claude Code session in this project directory. Claude
   Code reads `.mcp.json` at startup, not while a session is already running.
2. Approve the server when prompted (first time only) - it's a subprocess
   Claude Code starts on your machine, so it asks for consent once.
3. Confirm it connected: run `/mcp` inside the session, or `claude mcp list`
   from a terminal.
4. Ask it to use the tools, e.g.: *"Search VectorAgent for a vector database
   icon, preview the best paid match, then fetch it and build a React status
   card with it."*

The six tools, and which of them can spend:

| Tool | Cost | What it does |
| --- | --- | --- |
| `search_vector_icons` | free | ranked matches with tier, price and `match_confidence` |
| `preview_vector_icon` | free | metadata + watermarked preview URL, reads the 402 without paying |
| `list_vector_icons` | free | catalog counts; pass a tier to list ids |
| `verify_vector_license` | free | resolves an SVG's `data-va-tx` against Horizon, never against us |
| `fetch_vector_icon` | **tier-priced** | returns one SVG, settling first if the icon is not free |
| `fetch_vector_icons` | **summed** | a basket of up to 40 in ONE settlement; prefer this |

Watch the resource server's terminal log the 402 -> verify -> settle sequence
in real time.

If you'd rather use the plain Claude Desktop chat app instead of Claude Code,
the same JSON block goes under `mcpServers` in `claude_desktop_config.json`
(Windows: `%APPDATA%\Claude\claude_desktop_config.json`), with `cwd` set to
this project's absolute path since that app doesn't infer it from where the
config file lives.

## Architecture

```
Claude (Code or Desktop)
   |  MCP tool call: fetch_vector_icons([ids])
   v
src/mcp-server.ts  (holds AGENT's Stellar keypair)
   |  HTTP GET, wrapped with @x402/fetch
   v
supabase/functions/vectoragent  (x402 resource server; npm run dev:edge locally)
   |  free icon  -> 200 + SVG, no challenge, stop here
   |  unknown id -> 404, no challenge, stop here
   |  paid icon  -> 402 + PAYMENT-REQUIRED header (price from the icon's tier)
   |               + body carrying the watermarked preview URL
   |  <- mcp-server signs a Soroban USDC transfer, retries
   v
OpenZeppelin-hosted x402 facilitator (channels.openzeppelin.com/x402/testnet)
   |  verifies + broadcasts the signed transfer, fees sponsored on testnet
   v
Stellar testnet ledger -- AGENT -> VAULT
   |
   |  the function returns the SVGs; mcp-server.ts stamps each one with the
   |  transaction that paid for it and hands them back
   v
A second, independent transaction writes the receipt: sha256 of the itemised
manifest, into a manage_data entry keyed by the payment hash. Soroban payments
cannot carry a memo, which is the only reason this is two transactions.
```

`site/` is a static build with no backend of its own: the settlements page
reads Horizon and the content store straight from the browser, so it is a
second witness to the same settlements rather than this project reporting on
itself.

### Why the paid route is not a Worker

Everything else here is static and could sit on one platform, so the split is
worth explaining. Cloudflare Workers allow 10ms of CPU per request on the free
plan. Signing one receipt transaction measures **7.5ms** on its own, before the
payment signing, the hashing or the catalogue search. A Supabase edge function
allows 2s, so that is where the paid route runs.

Two things do not survive that runtime and are handled rather than ported:

- `@resvg/resvg-js` is a native binding Deno cannot load, so previews are
  pre-rendered into the static site by `npm run build:previews` and
  `/preview/:file` redirects to wherever `PREVIEW_BASE` points.
- Express does not run on Deno, so routing is a small hand-written router. The
  x402 paywall itself ports cleanly, because `@x402/express` is a thin wrapper
  over the framework-agnostic `@x402/core/server`.

There is one implementation, not two. An Express server did this first and was
deleted once the function worked, because two versions of ten routes drift and
nothing tells you when.

## Scope

Built and working: the payment loop, tiered pricing, the asset library,
watermarked previews, baskets, the license attribute, the on-chain receipt,
in-browser verification, and a live demo that spends real testnet money.

Still out, and deliberately so:

- **Binding the citation to the asset id.** The license attribute ships, but
  the transaction itself does not carry the asset id, so the "which icon did
  this buy" link still depends on our records. See the section above.
- **Semantic search.** `src/search.ts` is a transparent lexical scorer, not a
  vector index. Good enough to exercise pricing; not good enough for
  "isometric 3D cloud database node". Two of the four test agents ignored its
  scores entirely and read the descriptions instead.
- **Creator ingestion and payouts.** The split, the publisher UI and the
  off-ramp are all unbuilt. Every asset here is one we authored, so there is
  no creator to pay yet.
- **Mainnet.** Testnet only, with a hosted facilitator. The test agents knew
  it, too: one of them called the funds "valueless faucet tokens" while
  deciding to spend them, which is a confound no amount of testnet proves
  away.

## License

[MIT](LICENSE), covering the code and the 1,550 icons alike. Use any of it for
anything.