Skip to content

Tokenized equities playbook

Status: Live (jurisdiction-gated)

Tokenized equities are the most tightly gated venue on the platform: eligibility is checked per wallet and per jurisdiction, and ineligible regions are refused at transaction-build time, not after. This playbook walks the full flow end to end, grounded directly in crank_mcp/tools/equity_tools.py: jurisdiction attestation, classification, quoting, the mandatory confirmation gate, execution, post-trade verification, and market-hours handling.

Every tokenized equity is a SPL token wrapping exposure to an underlying security; trading it is a Jupiter-routed spot swap under equity-specific guardrails -- there is no separate "equity engine."

1. Discover what is listed

markets = await client.call_tool("equity_markets", {})
# -> {"count": N, "markets": [...], "market_hours": {...}, "disclaimer": "..."}

Each market row carries symbol, name, mint, issuer, regulatory_framework, plus best-effort live price_usd / volume_24h_usd / market_cap_usd (degrades to null if market data is unavailable -- never fails the read).

2. Check classification before quoting

classification = await client.call_tool("get_token_classification", {
    "mint_or_symbol": "AAPLX",
})

Read-only, resolves a mint or symbol against the token registry and returns every classification dimension (backing, liquidity tier, issuer verification, market-data quality) plus a derived composite risk gate. This is fail-safe by design: a registered-but-not-yet-scored token reads as blocked until the daily reclassification task verifies it, and an unregistered token is always blocked -- nothing is implicitly tradeable. A blocked composite risk refuses trade_equity outright later in the flow; check this first so your agent does not build toward a dead end.

3. Jurisdiction attestation (Reg S)

Tokenized equities on Crank run under a Reg S framework: no US persons, and an unknown jurisdiction is DENIED fail-safe (absence of proof is not permission). Declare once, standalone of any trade:

await client.call_tool("declare_jurisdiction", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "jurisdiction": "SG",   # ISO-3166-1 alpha-2, self-declared, never inferred
})

The attestation persists 90 days and is re-used by every gated call (trade_equity, perps, shorting) unless you pass jurisdiction inline on that call instead. trade_equity also accepts jurisdiction directly, and ip is checked as an additional Reg S signal (an OFAC SDN screen runs on the wallet address in both cases) -- neither the jurisdiction code nor the IP is ever inferred or offered as advice (hard rules 2, 5-8).

4. Market hours (24/7 on Solana, NYSE-aware)

hours = await client.call_tool("equity_market_hours", {"symbol": "AAPLX"})

Tokenized equities trade 24/7 on Solana -- the structural advantage over the underlying's NYSE session. This tool returns the NYSE session status (open/closed, next open/close) purely as context; equity_quote and trade_equity both still succeed with the market "closed" (they attach an informational market_closed disclosure rather than refusing the trade).

5. Quote first (free, non-binding)

quote = await client.call_tool("equity_quote", {
    "symbol": "AAPLX",
    "amount": 500_000_000,   # USDC base units (6dp) on a buy
    "side": "buy",
    "slippage_bps": 50,
})

Returns expected output, price impact, the effective technology-service-fee breakdown (fee_model, flat for equities -- no staker/pay-in-$CRANK discount affordances on this rail, crypto-only per the fee schedule), NYSE market-hours context, and the standard equity disclaimer. This is a non-binding preview -- no execution, no confirmation gate.

6. Trade: the three-call confirmation gate

trade_equity enforces no autonomous equity execution -- every trade needs an explicit human/agent confirmation step before anything is built.

Call 1 -- preview (no confirm):

preview = await client.call_tool("trade_equity", {
    "symbol": "AAPLX",
    "side": "buy",
    "amount": 500_000_000,
    "wallet_address": "<AGENT_PUBLIC_KEY>",
})
# -> {"action": "confirmation_required", "geo": {...}, "risk_warnings": [...], ...}

Runs the full guardrail chain before returning anything: classification gate (blocked composite risk raises CLASSIFICATION_BLOCKED), Reg S geo-gate + OFAC screen (raises GEO_RESTRICTED), token-authenticity verification including the issuer's published metadata authority (an impersonated equity mint is blocked outright), and a price-impact guard (raises PRICE_IMPACT_EXCEEDED above EQUITY_MAX_PRICE_IMPACT_PCT). Without confirm=true it stops here with action: "confirmation_required".

Call 2 -- build (confirm=true, no signed_transaction):

build = await client.call_tool("trade_equity", {
    "symbol": "AAPLX",
    "side": "buy",
    "amount": 500_000_000,
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "confirm": True,
})
# -> {"action": "sign_required", "transaction": "<base64>", ...}

Same guardrail chain re-runs, then returns an unsigned transaction for your agent's own wallet to sign. Crank never signs (hard rule 1).

Call 3 -- execute (signed_transaction set):

executed = await client.call_tool("trade_equity", {
    "symbol": "AAPLX",
    "side": "buy",
    "amount": 500_000_000,
    "wallet_address": "<AGENT_PUBLIC_KEY>",
    "confirm": True,
    "signed_transaction": "<BASE64_SIGNED_TX>",
    "verify": True,
})
# -> {"action": "executed", "tx_signature": "...", "verification": {...}}

Broadcasts the caller-signed bytes and records the fill. With verify=True (default), Crank awaits on-chain confirmation and re-reads the equity mint's post-trade balance, returned as verification -- gate any follow-on sizing decision on verification.confirmed, never on tx_signature presence alone. Set verify=False to skip the wait for latency-sensitive callers (the trade still broadcasts).

Param Type Default Meaning
symbol string required Tokenized-equity ticker (e.g. AAPLX).
side string required "buy" or "sell".
amount int required Base units of the INPUT token: USDC (6dp) on a buy, the equity token on a sell.
wallet_address string required Agent's managed wallet address.
jurisdiction string none Inline attestation instead of a prior declare_jurisdiction call.
confirm bool false Must be true to build a transaction -- no autonomous execution.
slippage_bps int 50 Max slippage.
signed_transaction string none Set on the execute call.
allow_unverified bool false Bypass token-authenticity verification (not recommended).
ip string "" Caller origin IP, an additional Reg S signal.
venue_hint string none Advisory only -- equity routes via the issuer registry, one surface today.
verify bool true Await on-chain confirmation + balance re-read on execute.

7. Track positions

positions = await client.call_tool("equity_positions", {
    "wallet_address": "<AGENT_PUBLIC_KEY>",
})

Filters the wallet's balances to registered equity mints with live USD pricing, issuer/regulatory metadata, and NYSE market-hours context. avg_cost and unrealized_pnl are null today -- cost-basis tracking is a documented follow-up (derivable from recorded swap history), not silently fabricated.

8. Corporate events (the after-hours-agent case)

events = await client.call_tool("equity_corporate_events", {"symbol": "AAPLX"})

Earnings, dividend, and split calendar for the underlying -- the events an overnight strategy reacts to while the tokenized wrapper trades 24/7 on Solana regardless of the NYSE clock. Returns an honest empty payload plus a SEC EDGAR link when no data feed is configured, never a fabricated calendar.

Mechanical automation: Equity DCA

To automate step 6 on a recurring schedule instead of driving it by hand, see the Equity DCA strategy guide -- DCA is classified mechanical (not discretionary), so it stays autonomous on an equity target even though trade_equity itself requires per-call confirmation; the strategy's config is what the owner already confirmed at creation time.

See also