Tokenized Stocks
How an AI agent buys and sells Coinbase's tokenized U.S. stocks on Base from its own wallet - the x402 routes, the calldata it signs, and a working script.
Coinbase's tokenized U.S. stocks launched on Base on 24 August 2026 as B20 tokens: ERC-20
compatible, eight decimals, one token tracking one share times an on-chain multiplier() that
absorbs splits and dividends. Secondary trading is permissionless, so any wallet can hold and trade
them. Ozmium quotes them, builds the calldata, and your agent signs it.
Ozmium never signs and never takes custody. You get { to, data, value } back and broadcast it
from your own wallet, exactly as with every other builder on this API.
The Three Routes
| Route | Price | What it returns |
|---|---|---|
GET /v1/stocks |
$0.001 | The catalog: every ticker, its token address, Chainlink feed, live USD price, feed age, and whether it is minted and tradable. |
GET /v1/stocks/price |
$0.001 | A priced quote for one order without the calldata. |
POST /v1/tx/stocks |
$0.001 | The quote plus the steps to sign. |
Pay with USDC over x402, or halve the price by paying in OZ. Surge pricing never applies to an OZ-credited call.
Buying
POST https://ozmium.org/v1/tx/stocks
Content-Type: application/json
{ "sym": "NVDAc", "side": "buy", "amount": "20", "taker": "0xYourAgentWallet", "slippageBps": 100 }
amount is human-scale: USDC to spend on a buy, shares to sell on a sell. The response carries
steps and an advisory:
{
"network": "eip155:8453",
"steps": [
{ "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x095ea7b3...", "value": "0" },
{ "to": "0x2626664c2603336E57B271c5C0b26F421741e481", "data": "0x04e45aaf...", "value": "0" }
],
"advisory": {
"amountOut": "8872500", "amountOutMin": "8783813", "pricePerShare": 225.41,
"feedUsd": 225.2579, "feedUpdatedAt": 1788471000, "vsFeedPct": 0.069, "multiplier": 1,
"pool": { "fee": 3000, "usdc": 23847, "shares": 54.86 }
}
}
Step one is the USDC approve, prepended only when the live allowance is short. Step two is
exactInputSingle on Uniswap's SwapRouter02, with your taker as the recipient. Broadcast them in
order, or batch them in one user operation if your wallet is a smart account.
Selling is the same call with "side": "sell" and amount in shares. The approve is then on the
stock token.
Why Uniswap and Not the Trade API
The CDP Trade API does not quote B20 tokens. Ozmium therefore measures the deepest USDC pool for the
token live, fills through QuoterV2, and returns the router calldata. The advisory shows the pool
it chose and its depth, so your agent can size the order against real liquidity rather than assume
it.
Read the Advisory Before You Sign
vsFeedPctis the pool fill against Chainlink's 24/5 total-return feed. A large positive number on a buy means you are paying a premium to thin liquidity. Refuse your own trade rather than signing it.feedUpdatedAtis the feed's own timestamp. Feeds freeze through corporate actions. A stale feed is a reason to wait, not a reason to widen slippage.pool.usdcandpool.sharesare the reserves you are trading against. Only four names are minted today, and pool depth is measured in tens of thousands of dollars, not millions.multiplierconverts token to share. Token price equals underlying times multiplier.
Unminted tickers return 409 with no pool rather than a fabricated quote.
Jurisdiction
These tokens are issued under Regulation S and are not offered to U.S. persons. Minting and redemption are gated by an on-chain authorization check; secondary transfers are not. Your agent is responsible for its own eligibility. Ozmium routes a swap and takes no view on who you are.
A Working Agent
This is the whole integration. It uses a bare private key, no Coinbase account, and no Ozmium credential of any kind.
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { base } from 'viem/chains'
import { x402Client, x402HTTPClient } from '@x402/core/client'
import { registerExactEvmScheme } from '@x402/evm/exact/client'
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY)
const wallet = createWalletClient({ account, chain: base, transport: http() })
const pub = createPublicClient({ chain: base, transport: http() })
const x = new x402Client()
registerExactEvmScheme(x, { signer: account })
const http402 = new x402HTTPClient(x)
async function paid(url, init) {
let res = await fetch(url, init)
if (res.status !== 402) return res.json()
const pr = http402.getPaymentRequiredResponse((n) => res.headers.get(n), await res.json())
const payload = await http402.createPaymentPayload(pr)
res = await fetch(url, { ...init, headers: { ...init.headers, ...http402.encodePaymentSignatureHeader(payload) } })
return res.json()
}
const order = { sym: 'NVDAc', side: 'buy', amount: '20', taker: account.address, slippageBps: 100 }
const { steps, advisory } = await paid('https://ozmium.org/v1/tx/stocks', {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(order),
})
if (Math.abs(advisory.vsFeedPct) > 1) throw new Error('premium too wide against the feed')
for (const step of steps) {
const hash = await wallet.sendTransaction({ to: step.to, data: step.data, value: BigInt(step.value) })
await pub.waitForTransactionReceipt({ hash })
console.log(hash)
}
The calldata arrives with the ERC-8021 builder code already appended. Do not re-encode it.
The Tickers
Thirteen names are registered. GET /v1/stocks reports minted and tradable per name, which is
the field to branch on: an unminted ticker has no pool and cannot be filled.
| Ticker | Underlying | Ticker | Underlying |
|---|---|---|---|
| AAPLc | Apple | MSFTc | Microsoft |
| AMZNc | Amazon | MSTRc | Strategy |
| COINc | Coinbase | NVDAc | NVIDIA |
| CRCLc | Circle | SNDKc | SanDisk |
| GOOGLc | Alphabet | SPCXc | SpaceX |
| INTCc | Intel | TSLAc | Tesla |
| METAc | Meta |
Humans get the same rail in the app: open any of these on Risk and pick Stocks beside Gains and Avantis.