The /api/v1 surface
Every endpoint on the REST API is also served under
/api/v1/ with the same data reshaped into a machine contract:
- Coded errors.
{"error":{"code":"rate_limited","message":"rate limited","retryAfter":21}}instead of prose - the code reference. - One list envelope. Every paged list carries
itemsandnextOffset- pass it back asoffset, stop atnull. - Idempotency echo. An
Idempotency-Keyheader is echoed back. - Every response carries
x-bstonk-api-version: 1.
Those two are canonical; this page is orientation. Both are
generated from one registry in the API’s own source, so they cannot
drift from what the server does. Treat any body shown here as an
example and the spec as the authority.
/api/v1 is where the
contract is promised. Responses evolve additively on both - ignore
unknown fields.
Budgets and refusals
A bearer buys, it never gates. Every read works without one. An identity changes which bucket you spend from:
With a bearer you spend from the wallet bucket and the envelope at
once; the tighter of the two is what
x-ratelimit-remaining reports. The
envelope exists because wallets are free to create - without it, one
address could mint sessions in a loop and print budget.
x-bstonk-identity tells you which budget answered - wallet when
the bearer was honoured, ip when it was absent, expired or invalid.
Reads still succeed in the ip case, so this header is the only way to
notice a bearer silently stopped working.
Error code reference
error.code is the stable value for program logic; message is
human-readable text that may improve over time. Branch on status and
code, never on message.
When
402 arrives it will carry an accepts array naming an x402
scheme.
Retry policy
- retry
429afterretryAfter - retry
500once; retry503with capped exponential backoff and jitter - do not blindly retry
400,401,403or404 - a
422is not a retry - it is a precondition to fix, named inverdict.checks - re-prepare after
expiresAtBlock; never sign a transaction past its expiry - a launch re-prepare is free of duplicates by construction - the idempotency key is the CREATE2 salt
Sessions: a wallet signature, briefly a bearer
A session keys your rate budget to your wallet instead of your IP - which matters the moment your agent shares datacenter egress with somebody else’s. Sign one message, exchange it once:{ name: "BaseStonk", version: "1", chainId: 8453 }, type
Authorization { action: string, address: address, expiresAt: uint256, detail: string } -
with detail empty and action = agent:session for the read tier or
agent:session:prepare for read + /prepare. The scope is part of what
the wallet signs, on purpose: a signature collected for reads cannot be
redeemed for the prepare tier.
Rules that will save you a debugging session:
- A signature mints exactly one session - a replay answers
401 signature already used. - Bearers last one hour.
GET /api/v1/agent/whoamisays which identity and scopes a bearer carries without spending a real read. - Minting is rationed at 3 per hour per IP.
- The same cost weighting applies; a
/preparecall costs 10 units on top of the shared lane’s 1.
Prepared transactions
Two endpoints assemble, check and simulate a transaction, then hand it to you unsigned. Both demand theprepare scope. The shared response shape:
verdict.checksis what was actually asked of the chain. A failed check answers HTTP 422 with the full verdict in the body - never a 200 with a quiet flag. Branch on status, then readchecks.gasis the estimate plus 25% headroom, ornullwhen the simulation failed.
approvalTx comes first
When a request moves tokens the contract must pull, the answer may carry
an approvalTx. Send it, wait for the receipt, then send tx. It is
a separate field rather than an array so the main transaction cannot race
its own precondition, and it approves exactly the amount needed, never
unbounded. It is null once the allowance suffices - re-preparing
after the approval lands is how you confirm you are ready.
Staleness, and why re-preparing is safe
expiresAtBlock is roughly five minutes out. Past it, re-prepare
rather than signing - the simulation was true at one block only.
Re-preparing a launch costs nothing: the idempotency key is the CREATE2
salt - keccak256(sender, salt) - the same value the chain itself
collision-checks. If the launch has not landed you get the same
transaction back; if it has, the verdict says so and steps to the next
free address. A trade’s idempotencyKey is null for the opposite
reason: two identical swaps are two legitimate trades, and retry safety
on a trade is your nonce, not our field.
Launch
eth_call plus a gas estimate against
the live launcher.
- The creator can only be your bearer’s wallet.
devBuyPairis raw units of the pair token; the intent reports it as pair amount, USD, andpercentOfSupplyAtOpen. A non-zero dev buy gets funding and allowance checks and anapprovalTxwhen short.
intent.leg names it:
You do not choose. A gated answer additionally carries
route,
issuerFactory, startSqrtPriceX96 and impliedCapUsd in the intent;
gate_admits is the check that fails when the cap is outside the gated
band. A pair that is neither registered nor routable comes back as a
422 with tx.data empty - there is genuinely nothing to sign.
Trade
amountIn is raw units of what you are spending: the pair token on a
buy, the launched token on a sell. The answer is a swap through the venue
router with minOut set from the quote and your slippage.
- Quoted at the token’s own tax, never a default. Taxes here range to
10%, and a quote at an assumed rate reverts as
TooLittleReceived- a failure that reads as a broken pool. A token with no rate on record refuses to quote rather than guessing. - The chain outranks the quote. The
eth_callfill is compared againstminOut- a stale quote fails the verdict instead of failing your transaction. - A fresh launch nobody has bought yet quotes buys from its entry boundary; a sell into it is refused - the empty side has nothing to sell into.
The loop, end to end
A caveat worth knowing before you debug it
BaseStonk sits behind a CDN that bot-scores traffic, and requests from datacenter address space can be challenged at the edge - before the API sees them at all. The tell is a403 with no JSON error envelope:
every refusal this API itself issues is JSON with an error.code. If you
hit one, it is the edge, not your bearer. A carve-out for the /api/v1
paths is pending.
