Using OvercastApp
OvercastApp is the facade that composes the whole SDK. You construct it once
from a chain-specific ProtocolFactory and use its surfaces everywhere; no
other wiring is required.
import { OvercastApp } from "@overcast-xyz/core";
import { solana } from "@overcast-xyz/solana-client";
const app = await OvercastApp.create({
factory: solana.withConfig({
rpcUrl: "https://api.mainnet.solana.com",
backend: "https://api.overcast.xyz",
apiKey: process.env.OVERCAST_API_KEY,
privateKey: process.env.OVERCAST_KEYPAIR,
}),
});
To construct
SolanaFactorydirectly you must supplychainName(anddomain) —new SolanaFactory({ chainName: "solana", domain, ... }). Prefer thesolana.withConfig(...)preset above.
Configuration
SolanaConfig extends the chain-agnostic OvercastConfig:
| Option | Default | Purpose |
|---|---|---|
rpcUrl | https://api.mainnet.solana.com | Solana RPC endpoint (Solana-specific). |
commitment | confirmed | Solana commitment level (Solana-specific). |
backend | https://api.overcast.xyz | Overcast backend base URL (REST + RFQ socket). |
socketUrl | backend | Override the socket base separately (e.g. "" for a same-origin proxy). |
apiKey | — (currently required) | API key the backend authenticates the app with. See below. |
privateKey | — | Wallet key: byte array, id.json contents, base58 (chain key) or 0x-hex. Omit for read-only. See note below. |
socketConfig | socket.io defaults | socket.io client options for the RFQ connection (e.g. transports). |
ackTimeoutMs | 30000 | Timeout (ms) for RFQ request acknowledgments. |
protocolSigner | derived from privateKey | Inject a custom OvercastProtocolSigner (e.g. remote signing). |
overcastApi | HTTP view over backend | Inject a custom OvercastView implementation. |
axios | — | Extra axios defaults for the HTTP view (baseURL always wins from backend). |
apiKey — currently required
The backend is authenticated: apiKey travels as an Authorization: Bearer
header on every REST read (app.api) and as the auth token on the RFQ socket
handshake (app.rfq). Leave it out and those two surfaces fail — 401 on
reads, a rejected handshake on the socket — regardless of whether the app has a
wallet. Only app.chain, which talks to the Solana RPC directly, is
unaffected.
See Getting an API key for how to obtain one.
privateKeynote: the chain signer accepts a byte array,id.jsoncontents, or base58 (Solana) /0x-hex (EVM). Protocol-signer derivation fromprivateKeyneeds a byte array /id.json/0x-hex — base58 currently throws there. If you use base58, injectprotocolSignerexplicitly.
The surfaces
One app, seven surfaces:
app.owner; // Bytes32 — your wallet identity / fee payer (see chain.canSign)
app.chain; // on-chain reads + signed writes
app.api; // curated off-chain REST reads
app.assets(); // this chain's curated asset metadata (lazy, async)
app.rfq; // realtime RFQ client (lazy)
app.settlement; // settlement-layer registry
app.utils; // chain-specific address helpers
app.chain — on-chain reads and writes
Reads:
await app.chain.balance(user, asset); // escrow account balance (asset: curated Asset)
await app.chain.offerExists(id);
await app.chain.optionExists(option); // takes full MarketOption
await app.chain.getOptionBalances(option); // { settlement, collateral }
await app.chain.exerciseClaims(option, holder); // taker's remaining claims
await app.chain.redeemClaims(option, holder); // maker's remaining claims
Writes (each builds, signs, and submits, returning Promise<Array<TxInfo>>
where TxInfo = { hash, confirmed, description? } — an array because an
operation is not always a single transaction):
await app.chain.deposit({ depositor, token, amount });
await app.chain.withdraw({ withdrawer, token, amount });
await app.chain.createOffer(offer);
await app.chain.cancelOffer(offer);
await app.chain.matchOffer(settlementOffer, collateralOffer); // mints the option
await app.chain.exerciseOption(option, amount, params?); // taker; params?: SettlementParams
await app.chain.redeemOption(option, amount, params?); // maker; params?: SettlementParams
app.api — curated backend views
Read-only, paginated queries against the backend's curated data (assets
resolved, amounts formatted for display). Every paged call returns
{ items, nextCursor?, total? } — except listAssets(), which returns
Asset[] directly:
await app.api.listAssets(); // Asset[]
await app.api.listOptions({ filter: { maker } }); // Page<CuratedMarketOption>
await app.api.listOffers({ filter: { asset, status: "open" } }); // unset status = open + closed; status: "closed" = everything not open
await app.api.listRfqs({ filter: { status: "open" } });
await app.api.listQuotes(rfqId); // listQuotes(rfqId: UUID, page?) — positional, not { rfqId }
Every list is scoped to the app's own domain by default — the backend serves
every domain it indexes, and an app points at one. State a domain on a
query's filter to read another one, or domain: undefined to read them all:
await app.api.listOffers({ filter: { domain: otherDomain } }); // one other domain
await app.api.listOffers({ filter: { domain: undefined } }); // every domain
app.assets() — resolved asset metadata
Everything on-chain names an asset by its canonical Bytes32 slot; humans and
UIs need the symbol and the decimals. app.assets() fetches the curated metadata
for this app's domain and hands back a view that resolves a slot in one
call:
const assets = await app.assets(); // one fetch, memoized
assets.get(slot); // Asset | undefined
assets.getOrDefault(slot); // Asset, address-only when uncurated
assets.bySymbol("USDC"); // pick by ticker
assets.list(); // every asset curated for this domain
assets.slot(asset); // back to the canonical Bytes32
assets.format(1_500_000_000n, slot); // "1.5 SOL"
assets.parse("1.5", slot); // 1_500_000_000n
assets.decimals(slot); // 9
Lookups are synchronous, so they are safe inside a render loop or an output
path. The promise is memoized — concurrent callers share one request — and a
failed fetch is not cached, so a retry works. Call app.refreshAssets() to pick
up metadata that has changed.
This is reference data, not a permission list. The protocol trades whatever
an offer's terms name, so an asset with no metadata still deposits, prices and
settles — getOrDefault hands back a bare Asset carrying just the address, and
amounts render in native units. Reach for require only where the metadata is
genuinely load-bearing, e.g. scaling a human-typed amount by decimals.
Assets are keyed by (domain, address), not by address alone: an address is only
unique within a domain. app.assets() pins the app's own domain, so you never
pass one.
Need the raw↔curated conversions rather than just lookups — turning an
OptionDetails off app.chain into the curated shape the backend would have
returned, or the reverse? await app.curator() gives you an OvercastCurator
sharing the same metadata.
app.rfq — the realtime RFQ client
app.rfq is a lazy getter: the socket connects on first access and the same
RfqClient instance is returned afterwards. It never throws for a missing
signer — only submitQuote and the signed path of acceptQuote require a
protocol signer (the chain path acceptQuote({ quoteId, chainOfferId }) does
not). registerRole, createRfq, and broadcasts work without one.
const rfq = app.rfq; // connects
await rfq.registerRole("TAKER"); // or "MAKER"
// ... see the RFQ quoter guide ...
rfq.disconnect(); // tears down
There is no app.start() / app.stop(): create() builds everything,
.rfq connects on demand, rfq.disconnect() is the only teardown needed.
app.settlement — settlement-layer registry
Resolves settlement-layer configs to pin into OptionDetails before signing:
app.settlement.types(); // ["physical", "oracle"]
await app.settlement.defaultConfig(details); // config Id for the default layer
// A layer derives the configuration itself. `configId` turns it into the id to
// pin, and `createConfig` into the transaction that deploys it — which is what
// you submit when the chain has nothing at that id yet.
const oracle = app.settlement.resolve("oracle");
const config = await oracle.defaultConfig(details);
const id = await oracle.configId(config);
Read-only mode
Construct the app without a privateKey and you get a fully functional
read-only client — app.api and the app.chain read methods work, while
writes and app.rfq are unavailable. This is the right shape for dashboards
and indexers. apiKey is still required: read-only drops the wallet, not the
backend authentication.
Why a factory?
@overcast-xyz/core is chain-agnostic: it defines the interfaces
(OvercastReader, OvercastWriter, OvercastChainSigner,
SettlementLayerRegistry, …) and OvercastApp composes whatever a
ProtocolFactory produces. SolanaFactory is the Solana implementation —
one config in, every per-chain piece out. Supporting another chain means
implementing one factory, not touching application code.