Architecture
Architecture Overview
The whole system on one page: who talks to whom, what happens inside the facilitator during a payment, and where each guarantee is enforced. Each section links to the page that covers it in depth.
By the end of this page you will know the four actors in an x402 payment, the order they act in, and which component enforces which property. Read this first if you are evaluating whether to depend on Vellar; read the linked pages when you need the detail behind a claim.
Vellar runs on stellar:testnet only. The hosted facilitator is
https://vellar-facilitator.onrender.com.
System Overview
flowchart LR
B["Buyer<br/>(AI agent or human)"]
S["Seller<br/>(resource server)"]
F["Vellar Facilitator<br/>verify · settle · catalog"]
L["Stellar Ledger<br/>Soroban + Horizon"]
B -->|"1 GET /resource"| S
S -->|"2 402 + terms"| B
B -->|"3 GET + PAYMENT-SIGNATURE"| S
S -->|"4 POST /verify, POST /settle"| F
F -->|"5 submit fee-bumped tx"| L
L -->|"6 settlement hash"| F
F -->|"7 hash + catalog result"| S
S -->|"8 200 + resource body"| B
The buyer never talks to the facilitator directly on the exact path: the
seller does, because the seller is the party deciding whether to hand over the
resource. The buyer signs an authorization entry rather than a transaction, so
the facilitator builds and pays for the transaction that carries it. The buyer's
address appears as neither the transaction source nor the fee payer, which is
the non-custodial property you can check on Horizon.
For the protocol step by step, see The payment loop. For what the buyer actually signs, see The exact scheme.
Settlement Path
sequenceDiagram
participant S as Seller
participant F as Facilitator
participant P as Channel pool
participant L as Stellar ledger
S->>F: POST /verify
Note over F: re-simulate, runs __check_auth<br/>and any spending policy
F-->>S: isValid: true
S->>F: POST /settle
F->>F: check sponsor vs hard floor
F->>P: acquire 1 of 50 channel accounts
F->>L: submit fee-bumped transaction
L-->>F: successful: true
F->>F: Bazaar catalog hook
F->>P: release channel account
F-->>S: settlement hash
The critical step is re-simulation at /verify. A signature proves the buyer
signed something; it proves nothing about whether that something can succeed.
Simulation runs the buyer's __check_auth under current chain state, so a
payment that would fail on-chain is caught before any funds move.
Cataloging happens on settle, never on verify, and a cataloging failure never changes the settlement result. See Settlement Path for every check in order, and Fees and Sponsorship for the three distinct quantities called "fee".
Channel Pool
flowchart TB
subgraph pool["Channel pool: exactly 50 accounts"]
C1["channel 1"]
C2["channel 2"]
CN["… channel 50"]
end
SET["Settlement"] -->|"acquire 1, exclusive"| C1
C1 -->|"inner tx source_account"| TX["Fee-bump transaction"]
SPON["Sponsor account<br/>(never in the pool)"] -->|"fee_account"| TX
TX --> LED["Stellar ledger"]
LED -->|"settled or failed"| REL["Release back to pool"]
REL --> C1
A Stellar account's sequence number must increment exactly once per submitted
transaction. One shared source account under concurrent load means two
settlements read the same sequence number, one wins, and the other fails with
txBadSeq. Giving each concurrent settlement its own account removes the shared
state entirely, so there is nothing to collide on and nothing to retry.
The count is enforced as exactly 50 at boot, not as a minimum: 49 behaves like a healthy pool until the 50th concurrent settlement, and extra keys are rejected rather than trimmed so an operator who supplies 60 is not left believing they bought capacity for 60. The sponsor is deliberately excluded, because it submits its own funding and fee-bump transactions.
See Channel Pool for the sizing argument, the monitor, and
/health interpretation.
Smart Account Policy
flowchart TB
AGENT["Agent session key"] --> L1{"Layer 1<br/>process ceiling"}
L1 -->|"over max_amount or<br/>session ceiling"| R1["Refused before signing<br/>nothing spent"]
L1 -->|"within ceiling"| SIGN["Sign auth entry"]
SIGN --> VERIFY["Facilitator /verify<br/>re-simulation"]
VERIFY --> CA["Wallet __check_auth"]
CA --> L2{"Layer 2<br/>spending-limit policy"}
L2 -->|"over cap"| R2["Error(Contract, #1)<br/>wrapped as #110<br/>nothing spent"]
L2 -->|"within cap"| OK["Co-sign, settle on-chain"]
The two layers are not redundant. Layer 1 lives in the same process the agent is
talking to and resets when that process restarts, so it guards against mistakes:
a typo, a runaway loop, a resource that costs more than expected. Layer 2 runs
inside the wallet contract's __check_auth, so the chain enforces it regardless
of what code submitted the transaction. A compromised agent, a modified SDK
build, or a hand-crafted envelope all meet the same contract.
Only layer 2 is a security boundary. The policy validates the token and the amount and has no opinion on the recipient, so "the agent cannot exceed its budget" is true while "the agent's funds are protected" is not.
See Spending Policies for the execution path and how
to read an opaque #110 refusal, and Policies
for deploying and attaching them.
Catalog Integrity
flowchart TB
SETTLE["Settled payment"] -->|"catalog hook on /settle"| CHECK{"URL already bound?"}
CHECK -->|"no"| BIND["TOFU: bind URL to this payTo"]
CHECK -->|"yes, same payTo"| UPDATE["Update entry"]
CHECK -->|"yes, different payTo"| REFUSE["binding_refused<br/>payment still settles on-chain"]
BIND --> VERIFY{"ownerVerified check:<br/>fetch the URL, is payTo<br/>in its 402 challenge?"}
UPDATE --> VERIFY
VERIFY -->|"yes"| OV["ownerVerified: true"]
VERIFY -->|"no or unfetchable"| UNV["unverifiable"]
A resource enters the catalog only after a real payment settles for it. There is
no registration endpoint and no submission form, so spam costs real funds moved
on-chain to the payTo in the entry's own challenge. The cost of an attack
scales linearly with its size, and the attacker pays it.
Ownership is trust-on-first-use, and the facilitator does not take the binding on trust from the payload: it fetches the URL and confirms the address appears in the 402 challenge the resource actually serves. What TOFU cannot do is establish who ought to have been first.
See Catalog Integrity for the five verification requirements, the metadata sanitisation rules, and the F11 controlled A/B test.
Search and Retrieval
flowchart TB
Q["Query"] --> LEX["Stage 1: lexical<br/>8 synonym groups, 6 stem rules<br/>serviceName 4x · tags 3x<br/>description 2x · URL 1x"]
Q --> SEM["Stage 2: semantic<br/>Voyage AI voyage-code-3<br/>1024 dimensions, cosine similarity"]
LEX --> RRF["Reciprocal Rank Fusion, k = 60<br/>1/(k+lexRank) + 1/(k+vecRank)"]
SEM --> RRF
RRF --> OUT["Ranked results<br/>ties broken by settlements x2 + uniquePayers"]
The two stages answer different queries. The lexical arm scores token overlap
and cannot rank a query that shares no vocabulary with any listing, because
there is nothing to score. The semantic arm handles exactly that case. RRF fuses
them by rank position rather than by score, so the two stages never have to
share a scale. At k = 60 the top of each list is flattened, so a resource must
rank well in both to beat one that is second in both.
Hybrid raised MRR on semantic queries from 0.264 to 0.717 while leaving keyword queries unchanged at 0.950, which is the whole argument for fusing rather than replacing. Those figures come from a 19-entry catalog and should be read as evidence the semantic arm changed something real on that corpus, not as a quality claim about search at scale.
A Voyage outage degrades the ranking without breaking the endpoint, and the response gives no indication that half the pipeline was missing.
See Search and Retrieval for the pipeline detail and Search Evaluation for the query sets.
