# Stocks

Requires `stocks:read` for the reads and `transactions:write` for the trade
flow - granted by name, or through the
[`all:read` / `all:write` wildcards](/authentication#wildcard-scopes).

Stocks are tokenized shares of real equities. They trade on-chain against a USD
stablecoin, they follow the **US market calendar**, and every endpoint here keys
off a `slug` in the same way the staking endpoints key off a pool slug.

The same catalogue also carries tokenized **commodities** such as gold. They go
through every endpoint on this page exactly as a stock does, and `assetType`
tells the two apart - see [Commodities](#commodities) for where they behave
differently.

| Endpoint | Scope | Returns |
| --- | --- | --- |
| `GET …/stocks/list` | `stocks:read` | The catalogue with live prices |
| `GET …/stocks/spot-price/{slug}` | `stocks:read` | One stock's price |
| `GET …/stocks/quote/{slug}` | `stocks:read` | What an amount would trade at |
| `GET …/stocks/fee-estimate/{slug}` | `stocks:read` | The approximate fee on an amount |
| `GET …/stocks/chart/line/{slug}` | `stocks:read` | Price history |
| `POST …/stocks/construct/buy/transaction/batch` | `transactions:write` | An unsigned buy |
| `POST …/stocks/construct/sell/transaction/batch` | `transactions:write` | An unsigned sell |
| `POST …/stocks/transaction/batch/send` | `transactions:write` | Submits a signed trade |

Holdings are read from the portfolio API:
[`GET …/portfolio/positions/open/stocks/list`](/portfolio#stock-positions).

## The catalogue

```bash
curl https://stg-s2s.restake.gg/api/v1/s2s/stocks/list \
  -H "authorization: Bearer $ACCESS_TOKEN"
```

```json title="Response"
{
  "data": [
    {
      "slug": "apple",
      "symbol": "AAPLon",
      "name": "Apple Inc.",
      "logo": "https://…/aapl.png",
      "chain": "BSC",
      "assetType": "equity",
      "category": "technology",
      "buyEnabled": true,
      "sellEnabled": true,
      "tradableSessions": ["premarket", "regular", "postmarket", "overnight"],
      "stockToken": { "symbol": "AAPLon", "chain": "BSC", "contractAddress": "0x…", "decimals": 18 },
      "paymentToken": { "symbol": "USDC", "chain": "BSC", "contractAddress": "0x…", "decimals": 18 },
      "price": 227.31,
      "sparkline": [225.8, 226.1, 226.4, 227.31],
      "change24hPercent": 0.58,
      "change24hUsd": 1.31,
      "marketOpen": true,
      "nextOpenAt": null
    }
  ]
}
```

`slug` is the handle every other call takes. `stockToken` is the share token,
`paymentToken` the USD stablecoin it trades against - you need the decimals of
both, because a buy is denominated in one and a sell in the other.

`assetType` is `equity` for a tokenized share and `commodity` for a tokenized
commodity. The list carries both unless you ask for one:

```bash
curl "https://stg-s2s.restake.gg/api/v1/s2s/stocks/list?assetType=equity" \
  -H "authorization: Bearer $ACCESS_TOKEN"
```

`?assetType=` takes `equity` or `commodity`; any other value is a `400`. It only
narrows the list - a slug it leaves out still works on every other endpoint.

<Callout type="caution" title="Integrated before commodities?">
  Without `?assetType=` the list now includes commodity rows. If your screens
  assume every row is a share - a sector per row, the US market calendar, a live
  price on the stream - pass `?assetType=equity` until they handle
  [commodities](#commodities).
</Callout>

`buyEnabled` / `sellEnabled` are Restake's own switches. They are independent of
whether the market is open: a side switched off is refused at any hour, while a
shut market is a route that does not exist yet.

`price` is the market price as the venue that issues these tokens reports it.
`sparkline` is hourly closes over the last 24 hours of open market with that
price appended, and `change24hPercent` / `change24hUsd` are measured over the
same window.

<Callout type="info" title="Polling faster does not get fresher numbers">
  Prices, series and changes are cached for a minute across all callers. Read
  the list once a minute; anything faster returns the same numbers. For prices
  as they move, subscribe to the [live price stream](/stream) instead.
</Callout>

One stock on its own, with the same fields:

```bash
curl https://stg-s2s.restake.gg/api/v1/s2s/stocks/spot-price/apple \
  -H "authorization: Bearer $ACCESS_TOKEN"
```

And its history, over `period` - `24H` (the default), `1W`, `1M` or `1Y`:

```bash
curl "https://stg-s2s.restake.gg/api/v1/s2s/stocks/chart/line/apple?period=1M" \
  -H "authorization: Bearer $ACCESS_TOKEN"
```

```json title="Response"
{ "data": [{ "time": 1788900000000, "value": 227.31 }] }
```

`time` is a unix timestamp in milliseconds. An **empty array is a real answer**,
not an error: it means there is no price history for that stock to draw.

## Market hours are the thing to design around

Most of what looks like a broken integration is the US market being shut.
Commodities keep their own hours - see [Commodities](#commodities).

Every stock carries `marketOpen` and `nextOpenAt` (ISO 8601, `null` while open).
Read them before you conclude anything is wrong, because while a market is
closed:

- the price does not move;
- `sparkline` and the `change24h*` fields describe the last 24 hours the market
  was *open*, so over a weekend they show the previous session's move, not zero;
- a PnL on a holding does not change;
- a quote or a trade **usually fails**, because there is no route to price
  against - this comes back as a `400`, not as a "market closed" error.

`tradableSessions` says which sessions a stock trades in - most trade the four
weekday sessions, a minority also trade weekends. It is there for display; do
not derive the schedule from it, because the windows carry US market holidays
and a single stock can be paused inside an open session for a dividend or a
split. `marketOpen` and `nextOpenAt` already account for both.

<Callout type="caution" title="Trades are not gated on the session">
  A buy or sell of a shut stock is still attempted, and fails upstream for want
  of liquidity. Refusing it here would be a second opinion about tradability,
  and the one that matters is whether a route exists. If you want to stop a user
  before that, branch on `marketOpen` yourself.
</Callout>

## Quoting

```bash
curl "https://stg-s2s.restake.gg/api/v1/s2s/stocks/quote/apple?side=buy&amount=250.5" \
  -H "authorization: Bearer $ACCESS_TOKEN"
```

```json title="Response"
{
  "data": {
    "slug": "apple",
    "side": "buy",
    "sourceToken": "0x…",
    "destinationToken": "0x…",
    "sourceAmount": 250.5,
    "destinationAmount": 1.1018,
    "executionPrice": 227.35523689,
    "spotPriceUsd": 227.31,
    "estimatedGas": 210000,
    "fee": {
      "notionalUsd": 250.5,
      "feePercent": 0.5,
      "systemFeeUsd": 1.2525,
      "gasCoverageUsd": 0.1,
      "totalFeeUsd": 1.3525,
      "feeAmountRaw": "1352500",
      "feeTokenSymbol": "USDC",
      "feeTokenDecimals": 6
    }
  }
}
```

`amount` is always in the token going **in**: the payment token on a `buy`,
shares on a `sell`. No wallet and no user are involved - a quote is a read.

`executionPrice` is what this amount fills at on the on-chain route, in payment
tokens per share, on either side. `spotPriceUsd` is the market price the
catalogue shows. The difference between the two is the route's spread and price
impact for this amount - show both if your users should see what a trade costs
beyond the fee. `spotPriceUsd` is `null` when the market price could not be
read; the quote itself is unaffected.

`fee` is what the trade would be charged on top of the route: `systemFeeUsd` is
the tiered platform fee - `feePercent` is the tier, and a larger trade falls in a
lower one. The tiers are the ones you set for your users in the partner panel
(**Fees**), or the platform's default tiers if you have not set any - and `gasCoverageUsd` is the gas Restake sponsors, priced from the
chain's current gas price. The two are summed into a single stablecoin transfer inside the batch,
which is what `feeAmountRaw` (base units, rounded up to the token's smallest
unit) settles. `notionalUsd` says which leg it was taken on: the dollars going in
on a buy, the route's proceeds on a sell. A `null` fee means the trade would be
charged nothing, not that the fee is unknown.

It is an **indication**. The number a trade actually executes at is the quote
returned alongside the batch that builds it, taken from the same routing call as
the transaction itself - and the fee on that quote is the one the batch charges.
Quote here to show a price; read the trade's own quote to confirm one.

## The trade flow

Buying and selling are **three steps**, exactly as
[staking](/staking#the-transaction-flow) is, and for the same reason: the API
never holds a user's key.

1. `POST …/construct/buy/transaction/batch` (or `…/sell/…`) returns an unsigned
   EIP-7702 batch user operation, an `intentId`, and the quote it was built
   against.
2. The user's wallet signs `bundle.userOpHash` and, when `bundle.authorization`
   is not `null`, the EIP-7702 authorization tuple.
3. `POST …/transaction/batch/send` returns the signatures with the `intentId`,
   and the API submits the operation.

The batch is atomic: the token approval, the swap and the platform fee execute
together or not at all. That is what makes a sell safe - the fee comes out of
proceeds the batch has not received yet, and a swap that returns less than
quoted reverts the whole operation rather than charging for a trade that did not
settle.

### 1. Build

```bash
curl -X POST https://stg-s2s.restake.gg/api/v1/s2s/stocks/construct/buy/transaction/batch \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "userId": "8f2b1c4e-…",
    "slug": "apple",
    "amount": 250.5,
    "slippage": 1
  }'
```

```json title="Response"
{
  "data": {
    "slug": "apple",
    "side": "buy",
    "quote": {
      "sourceAmount": 250.5,
      "destinationAmount": 1.1018,
      "executionPrice": 227.35523689,
      "spotPriceUsd": 227.31,
      "estimatedGas": 210000,
      "fee": { "totalFeeUsd": 1.3525, "feeTokenSymbol": "USDC" }
    },
    "intentId": "b7c1…",
    "bundle": {
      "chainId": 56,
      "entryPoint": "0x…",
      "delegate": "0x…",
      "calls": [{ "target": "0x…", "value": "0x0", "data": "0x…" }],
      "userOp": { "sender": "0x…", "userOpHash": "0x…" },
      "userOpHash": "0x…",
      "authorization": { "chainId": 56, "address": "0x…", "nonce": 0 }
    }
  }
}
```

`userId` names the user the trade acts for - a partner token identifies your
organisation, not a person. It must be one of your users; anything else is a
`404`.

`slippage` is a percentage between `0.1` and `5`, defaulting to `1`.

Gas is always sponsored: the user's address never needs the chain's native
token. The platform fee includes a gas-coverage component that pays for it,
priced from the chain's current gas price.

#### Trading the largest amount (`maxAmount`)

Send `"maxAmount": true` instead of `amount` to trade the largest amount the
wallet can afford right now. `quote.sourceAmount` in the response is the amount
used.

- Buy: the payment-token balance less the platform fee. A tiny remainder stays
  in the wallet, so a gas price that rises before the batch lands cannot make
  it unaffordable.
- Sell: every share the wallet holds. The fee comes out of the proceeds and is
  checked against the route's minimum return, as on any sale.
- `amount` is ignored when `maxAmount` is `true`.

### 2. Sign

The wallet signs identically to the [staking flow](/staking#2-sign):

- `signature` - EIP-191 over the 32 bytes of `bundle.userOpHash`, prefixed with
  `0xff00`.
- `authorization` - the EIP-7702 tuple signed as `{ r, s, yParity }`, using
  `bundle.authorization`. Only when `bundle.authorization` is not `null`; for
  an already-delegated wallet it is `null`, and the submit body leaves
  `authorization` out.

[Signing with Privy](/privy) and [Signing with Wallet Core](/wallet-core) cover
both signatures in code.

### 3. Submit

```bash
curl -X POST https://stg-s2s.restake.gg/api/v1/s2s/stocks/transaction/batch/send \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "userId": "8f2b1c4e-…",
    "intentId": "b7c1…",
    "signature": "0x…",
    "authorization": { "r": "0x…", "s": "0x…", "yParity": 0 }
  }'
```

```json title="Response"
{ "data": { "userOpHash": "0xa1b2…" } }
```

An intent is **single-use and short-lived**. A replayed or late submit is a
`400` - rebuild and re-sign rather than retrying the same `intentId`.

This endpoint is separate from staking's `…/staking-pools/transaction/batch/send`
even though the two carry the same payload: submit a trade through the endpoint
that built it.

## Estimating the fee (`fee-estimate`)

`GET …/fee-estimate/:slug?side=buy|sell&amount=…` answers the platform fee and
gas fee on an amount, and what is left of it once they are taken. It reads no
wallet and asks no route, so it is cheap enough to call as the user types. The
response has the same shape as on
[staking](/staking#estimating-the-fee-fee-estimate).

```bash
curl "https://stg-s2s.restake.gg/api/v1/s2s/stocks/fee-estimate/apple?side=sell&amount=2" \
  -H "authorization: Bearer $ACCESS_TOKEN"
```

- A buy takes `amount` in payment-token units. `finalAmount` is `amount` less
  the fee, so for a wallet holding `amount` it is about what `maxAmount` will
  spend.
- A sell takes `amount` in shares. The proceeds are valued at `spotPriceUsd`,
  before the route's spread. `finalAmount` is those proceeds less the fee, in
  the payment token.
- The figures are **approximate**. For the route's own price use
  [`quote`](#quoting); the construct call charges the exact fee.

## The units trap

The same trap as staking's, in a different pair of tokens:

| Direction | `amount` is denominated in |
| --- | --- |
| Buy | the **payment token** - `paymentToken.decimals` |
| Sell | the **stock token** (shares) - `stockToken.decimals` |

A buy is sized in the money going in; a sell is sized in the shares going out.
`amount` is a human-readable number (`250.5`, `1.1018`), not base units - the
API scales it by the decimals of the correct token.

<Callout type="caution" title="Selling a whole holding">
  Read the user's share balance from
  [`…/portfolio/positions/open/stocks/list`](/portfolio#stock-positions) and pass
  that `amount` straight through. Converting a USD figure back into shares
  through a price you fetched yourself will be wrong by the time the trade lands.
</Callout>

## Commodities

Rows with `assetType: "commodity"` are tokenized physical commodities. Today
that is Tether Gold (`XAUt`, slug `xaut-usdc-bsc`), a token backed by one troy
ounce of gold. Reads, quotes and the three-step trade are the same calls as for
a stock, and the same units rule applies - a sell is sized in the commodity
token. What differs:

- **`category` is `other`.** Sectors describe companies; group commodities by
  `assetType` instead.
- **Their hours are their own, not the US calendar.** XAUt trades whenever the
  chain does: it is always `marketOpen: true`, `nextOpenAt: null`, with an empty
  `tradableSessions`. Branch on `marketOpen` as you would for a stock, not on
  `assetType` - a commodity issued as a US-listed product would keep US hours.
- **Decimals are the token's own.** XAUt has 6, not the 18 of the tokenized
  shares. Read `stockToken.decimals`; never assume it.
- **The price is a market-wide reference.** For XAUt, `price` and a quote's
  `spotPriceUsd` are the gold token's price across markets, not the price of the
  pool the trade fills in, so the gap to `executionPrice` can be wider than for
  a stock. Show the quote before a user commits.
- **XAUt is not on the [live price stream](/stream).** Subscribing to it
  succeeds, but no update ever arrives. Read its price from the list or
  `…/stocks/spot-price/{slug}` - once a minute is as fresh as it gets.

## Which stocks you see

Every stock and commodity Restake lists is available to you by default, new ones
included - there is nothing to opt into. Your team can switch individual stocks
off in the partner panel, as with [pools](/staking#which-pools-you-see). A stock
switched off leaves `…/stocks/list`, and its spot price, quote, fee estimate,
chart, buy and sell answer `403` with `STOCK_DISABLED`.

That is your own configuration, not a permission we set, so the fix is a toggle
in the panel rather than a support ticket. A change takes effect within a minute.
A trade built before the switch can still be submitted, because
`…/transaction/batch/send` does not look at the stock again.

The [price stream](/stream) does not apply your selection. It serves every listed
stock to every connection.

## Failure modes

| Status | Cause |
| --- | --- |
| `400` | An unknown `assetType` on the list, that side is switched off for the stock, the user has no linked wallet, the wallet cannot afford the trade and its fee ([`INSUFFICIENT_BALANCE`](/errors#insufficient-balance) - checked before routing, so it costs no quote), no route exists for the amount (a shut market is the usual reason), or the intent is unknown, already used or expired |
| `403` | Your token lacks `stocks:read` or `transactions:write`, or your team has switched this stock off in the panel (`STOCK_DISABLED`) |
| `404` | No such stock slug - or no such user, or a user belonging to another partner |
