Skip to Ozmium documentation
Ozmium Docs

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

http
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:

json
{
  "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

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.

javascript
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.

Contents

On This Page