Skip to main content
Stellar is the only chain here where the spend limit is enforced by a contract before the payment exists, and the only one where a buyer pays for a trust check without holding a single unit of gas. Both networks run the same wasm and the same rail code: stellar:pubnet is live, stellar:testnet is beta.

Why Stellar

Native Circle USDC

Real USDC, issued by Circle, reachable from Soroban as a SEP-41 Stellar Asset Contract. Seven decimals, not the six every EVM USDC uses.

The buyer signs, not the transaction

A Soroban authorization ENTRY is signed for one specific transfer call. The seller assembles, pays the network fee and submits, so the buyer never touches a sequence number and never pays a stroop.

A policy the ledger enforces

AgentSpendPolicy is ours, written in Rust for Soroban rather than translated. A daily cap, an auto-approve ceiling, an allowlist, a freeze and a session-key expiry, with typed errors a client can branch on.
Stellar pubnet is live, and live has never meant “no caveats”. Four of them, stated up front:
  • Real money, but small money. The pubnet vault’s policy is 1 USDC per UTC day and 0.25 USDC per payment, an order of magnitude under the testnet vault, because this one holds value.
  • Both sides of every payment have been ours so far. All three x402 sales on pubnet, one from an operator machine on 2026-08-27 and two through the hosted deployment, had our payer and our payee. That is evidence the rail works, not evidence of demand, and the proof page says so.
  • The contract is not audited. Free tooling we can re-run, an adversarial review that found and fixed real defects, and a negative-control runner that deletes each guard in turn and requires the suite to go red. None of that is an audit and this project does not call it one.
  • No identity anchor. ERC-8004 is EVM-only. A Stellar agent’s passport is bridged from an EVM chain, and KYA cannot be anchored here at all.

Network config

Two properties of that table are worth stating rather than leaving implicit. The SAC id is derived, never pasted. A Stellar Asset Contract id is a deterministic function of the classic asset plus the network passphrase, which is why the same asset code maps to a different contract on each network. Both ids above came out of stellar contract id asset and were then read back live for symbol(), decimals() and name(). On pubnet the issuer was checked first, because anyone may issue an asset called USDC: GA5ZSEJ... publishes circle.com as its home domain, which is the SEP-1 binding between an issuer and a company. Reads fail over across the RPC list; a sendTransaction never does. A resubmitted envelope answers DUPLICATE on the second host, and the rail would read that as a decided refusal of a payment that is in fact in flight.

What is deployed

The vaults are ours. The USDC SACs are Circle’s asset seen through Soroban. The CCTP contracts are Circle’s, and the registry cites the Circle page each row was read from. The Stellar 8004 rows belong to TrionLabs: we read them, labeled third-party, and they are not our anchor. See Identity on Stellar below. The two OpenZeppelin rows are theirs as well: the smart account instance is ours, the account wasm and the WebAuthn verifier behind it are third-party testnet deployments we read out of the smart-account-kit manifest rather than built or verified. See Passkey owner below. All three vaults carry the same wasm, sha256 155eb31c1867254eacbf1b7a4755164d15cc6b6f939644705ab6b8df61579239, byte for byte, so pubnet runs the code that has been under test since the first testnet deploy rather than a rebuild of it. The pubnet vault was deployed 2026-08-24, the first testnet vault 2026-08-15, and the passkey-owned testnet vault 2026-09-19.

The spend vault

AgentSpendPolicy holds an agent’s USDC and refuses anything the owner did not permit. It has no upgrade entrypoint and no initialize: the constructor ran atomically at deploy and its arguments are permanent. There has been no upgrade, no re-initialization and no owner transfer on either network.

The gate order, and why order is the product

The ladder runs in a fixed order, and the order is observable behaviour rather than an implementation detail: an agent told Frozen when it expected DailyCapExceeded routes to the wrong recovery path. Both entry points first refuse an invalid payee, the vault itself or the settlement token. For an operator payment the ladder then runs amount, freeze, session-key expiry, allowlist, auto-approve ceiling, daily cap, and finally balance. That last pair is worth reading closely. When the pubnet vault’s budget was spent to its edge, the next payment was refused with DailyCapExceeded even though the vault was also short of funds and InsufficientBalance was available, because the cap gate fires first. The agent is told its budget is spent rather than that the account is short, and those call for different human responses. owner_pay is a human act, so it skips the ceiling, the allowlist and the freeze. It does not skip the payee check, the amount guard, the arithmetic or the balance check, and the amount it moves is still added to the day’s total, so on-ledger accounting stays honest about total outflow. These discriminants are frozen public ABI. A client decodes an integer, not a name, so the list is append-only.
A refusal has no transaction hash on Soroban, and that is the platform rather than evasion. A payment the contract refuses fails in SIMULATION, so nothing is submitted and no hash exists. The typed error code is the artifact. Anyone can reproduce a refusal against the live contract in seconds, and it costs nothing precisely because it is refused.Two failures have no code at all, and a client must expect a host trap rather than a numbered error: a failed require_auth() is a host panic, and a failed SAC transfer panics inside the token contract.

Archival, which is a calendar item rather than a risk

Soroban charges rent, and a contract instance whose rent lapses is archived rather than lost. The pubnet vault’s instance archives around 2027-01-06 unless it is touched in time, and a touch only counts once the remaining life is under the contract’s re-extension threshold of 1,036,800 ledgers. The contract calls that 60 days, counted at 17,280 ledgers a day, but at the measured 5.625 s close it is about 67.5 days, so the action window opens around 2026-10-31 rather than in November. A weekly GitHub workflow turns red at 45 days remaining rather than waiting for the deadline.

Owner controls, signed by the owner

The console reads live vault state and prepares owner calls: set_policy, set_frozen, set_allowed, set_session_key_expiry, withdraw and owner_pay. The owner signs each one with their own Stellar wallet and the server never holds the key. A per-agent Soroban vault can also be provisioned on testnet: it instantiates against the existing code entry rather than re-uploading the wasm, which is the difference between about 0.1 XLM and about 12 XLM. Pubnet provisioning is gated behind STELLAR_VAULT_ALLOW_PUBNET. There are two ways to be that owner, and the second one has no seed phrase in it. An owner can be a plain G... account signing with a Stellar wallet, which is what both vaults in the table above use. Or the owner can be a contract, because owner.require_auth() does not care which: Soroban dispatches to a contract owner’s __check_auth, so a smart account whose only signer is a passkey can hold the same authority. That is not a design note any more. It ran on testnet on 2026-09-19, and the next section is the record.

Passkey owner

A spend vault whose owner is a passkey. No seed phrase, no browser extension, no key this server could sign with even if it wanted to. The owner is an OpenZeppelin smart account contract whose single signer is a WebAuthn P-256 credential, and the vault’s owner.require_auth() is satisfied because that contract is the direct invoker of the call.

What ran, and what it cost

The vault is CBGTXWFB...NS6J6U, carrying wasm 155eb31c...9239, the same code the other two vaults run. Its owner is CC5RNXNH...VM3RWA, an instance of OpenZeppelin’s account wasm 1b5f4534...785a with their WebAuthn verifier CC7EKIHQ...ZIOM3F behind it, deployed through the smart-account-kit npm package 0.8.0. The whole record, with fees and the live reads taken afterwards, is in soroban/releases/testnet-passkey-owner-2026-09-19.json.

Why this is a second vault rather than a migration

AgentSpendPolicy has set_operator and deliberately no set_owner. The owner is permanent, which is the point of it, so an existing vault cannot be handed to a passkey. The vault above was deployed with the smart account as its owner from its first ledger, and CAIL6ECR...OEB4UI keeps its G... owner. Neither upload was needed: both instantiate against the code entry already on the ledger, about 0.1 XLM rather than about 12.

KYA maps onto a binary allowlist, and that is worth saying exactly

The trust verdict has three states and set_allowed has two, so the mapping is published rather than implied:
  • ALLOW writes set_allowed(payee, true). The agent can pay that payee inside the cap.
  • WARN writes nothing on chain. It is a server-side flag on the decision record.
  • DENY writes set_allowed(payee, false), after which pay() to that payee reverts with PayeeNotAllowed, error 3.
Nothing here should be read as three states living on the ledger. The ledger holds a boolean per payee, and a WARN that nobody acted on looks exactly like a payee nobody has ever mentioned.
Testnet only, and the passkey in the record was software. The P-256 key in the 2026-09-19 run was generated inside the process rather than held by a platform authenticator: the same verifier contract, the same WebAuthn signature format of authenticatorData, clientDataJSON and a low-S DER signature, and the same execute path. The only difference is where the private key lives, which means the run proves the contract path accepts a WebAuthn signature and proves nothing about a real authenticator’s user-verification prompt.Five more caveats, in the order they would bite:
  • One passkey is one signer. The smart account was created with a single WebAuthn signer and no recovery signer. Losing the passkey loses the owner, and since there is no set_owner there is no path around that: the balance would be unrecoverable. Adding a recovery signer is the mitigation and it was not applied here.
  • The kit is unaudited by its own statement, the OpenZeppelin Stellar contracts under it were audited at rc-v0.7.0 with a different scope, and the deployed artifacts use a later source revision than that audit. Our AgentSpendPolicy is not audited either.
  • The verifier and the account wasm are third party. We deployed one instance against ids read from someone else’s manifest. We did not build or verify them.
  • Two fee payers, and they are separate claims. The release record above was paid by a dedicated testnet deployer key over direct RPC. The public flow sponsors fees through OpenZeppelin Channels instead, and that path was proven separately the same day: the smart account at ledger 4764131, set_policy at 4764136, the owner’s allowlist writes at 4764142 and 4764144, and the agent’s payment at 4764146, every one of them sourced and paid by a Channels channel account rather than by the visitor. Neither run makes the other’s claim.
  • A fresh vault starts open. allowlist_enabled is false and the allowlist is empty until an owner call turns it on. Fund a vault before configuring it and there is a window in which the allowlist protects nothing.

Pay for a trust check

Four tools sell on this rail. The base price is identical on every chain we sell on: Ask for one without paying and you get a 402 carrying both networks:

Two payment schemes

soroban-auth is the default and the one to use. You sign a Soroban authorization entry for one specific transfer call on the USDC SAC, hand it over base64 in X-PAYMENT, and pay no network fee. The nonce inside that entry is what ties a transaction on the ledger to this purchase. settled exists for contract payers only. An agent whose spending a vault already bounds cannot sign an authorization entry, because a contract has no key. That agent calls pay() on its vault and hands us the resulting transaction hash instead. The binding is then weaker and the rail says so in the challenge itself: we can prove the payment happened and that nobody has redeemed it here before, but not that it was made for this particular purchase rather than another of the same price, and not that the presenter is the party who made it. A landed transaction is public, so a hash should be presented promptly. That is why this is the second shape and not the default.

What “settled” means here

A sale counts only once we read the SEP-41 transfer event back off the ledger and match it to the buyer’s authorization nonce. Not the facilitator’s word, not a 200 from anyone: our own read of the event, with {sac, to, from, amountRaw, authNonce} all required. An earlier version keyed only on the contract, recipient and amount, and an adversarial review broke it immediately with a real unrelated transaction that had moved the same number of base units. The nonce is what closed that.

The fee, in the units the ledger uses

The buyer pays nothing. We do, and rather than adding a settlement line item to a sub-cent sale we absorb it, which is a decision rather than a free lunch. The bid is the maximum offered into the inclusion auction; the ledger charges what it charges, and Horizon’s fee_charged is the figure that left the account. Recorded with the part that did not work: the first two pubnet attempts bid the 100-stroop minimum that testnet always accepts, lost pubnet’s auction and sat invisible until they expired. The break-even on the cheapest tool is XLM at 0.4353 USD: above that, one settlement costs more than a verify_agent sale. XLM has traded above that before, so this is a price move and not a hypothetical, and the decision gets revisited rather than discovered.
The buyer pays no fee, and still needs XLM to exist. A Stellar account requires a 1 XLM base reserve, plus 0.5 XLM per trustline, and it needs a USDC trustline before it can hold a single unit of USDC. That is a property of the chain no rail can remove. An operator who funds an agent with USDC alone will find the account was never created. The same applies to our payee: a payTo without a USDC trustline fails every settlement with op_no_trust, which reads to a buyer as our bug.

Sign in with a Stellar wallet

An owner signs in, or links a wallet to an existing account, with SEP-43 signMessage through Stellar Wallets Kit (@creit.tech/stellar-wallets-kit 2.6.0). Freighter signs SEP-53. The same account can carry wallets from several chain families at once, so an EVM-anchored passport and a Stellar signing key belong to one owner rather than two.

USDC across chains (CCTP)

Circle’s CCTP moves native USDC between Stellar and the EVM chains here by burn and mint, never a wrapped twin. Stellar is CCTP domain 27, and a Stellar recipient must ride in the hook data and be minted through Circle’s CctpForwarder: the message format has no room for a StrKey type marker, so a direct mint to a G... account is unrecoverable. Proven both ways on testnet on 2026-09-10; mainnet is opt-in only and capped. Details on Circle CCTP.

Identity on Stellar, stated plainly

There is no ERC-8004 on Stellar, because that standard is EVM-only. A Stellar agent’s passport is bridged from an EVM chain rather than anchored here, and KYA cannot be anchored on Stellar at all. We say so at the point of sale rather than reporting a zero that reads like a real count. TrionLabs’ Stellar 8004 is a real Soroban agent registry on both networks, and since 2026-09-15 we read it, read-only and labeled third-party. It is not our anchor, and three differences are the reason:
  • It mints its own ids in its own space. An id there and an ERC-8004 token id are different identities that happen to both be integers, so the labels stay explicit: stellar:{testnet|mainnet}:{registry}#{id}.
  • Its exported interface binds no foreign-chain identity. There is no CAIP-10 and no chain id; the only places a cross-chain reference could live are the free-form agent_uri and set_metadata, which are assertions by whoever holds the key.
  • It is upgradeable behind a timelock and owned by Trion. Ours deliberately is not upgradeable.
A-Identity is agent 25 on its testnet registry (tx 60701278...54b07, 2026-09-08). Mainnet registration is not done: a write simulation on 2026-09-09 reported their mainnet registry instance archived, with a 36.6 XLM restore in the footprint. Whether that is still true is a live read rather than a claim in this page.

Verify it yourself

The human-readable version of the first one is /proof/stellar.

Where Stellar fits

Arc holds identity and escrow, which Stellar cannot. Base and Arbitrum One run the EIP-3009 rail, where the same tools sell with a disclosed settlement fee because gas is the buyer’s cost to avoid. Stellar is where the policy itself is on the ledger, and where a buyer pays without ever holding gas.