Skip to main content

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 { SolanaFactory } from "@overcast-xyz/solana-client";

const factory = new SolanaFactory({
rpcUrl: "https://rpc.sandbox.overcast.xyz",
backend: "https://api.sandbox.overcast.xyz",
apiKey: process.env.OVERCAST_API_KEY,
privateKey: process.env.OVERCAST_KEYPAIR,
});

const app = await OvercastApp.create({ factory });

Configuration

SolanaConfig extends the chain-agnostic OvercastConfig:

OptionDefaultPurpose
rpcUrlhttps://rpc.sandbox.overcast.xyzSolana RPC endpoint (Solana-specific).
commitmentconfirmedSolana commitment level (Solana-specific).
backendlocalhostOvercast backend base URL (REST + RFQ socket).
socketUrlbackendOverride the socket base separately (e.g. "" for a same-origin proxy).
apiKey— (currently required)API key the backend authenticates the app with. See below.
privateKeyWallet key: byte array, base58, or id.json contents. Omit for read-only.
socketConfigsocket.io defaultssocket.io client options for the RFQ connection (e.g. transports).
ackTimeoutMs10000Timeout for RFQ request acknowledgments.
protocolSignerderived from privateKeyInject a custom OvercastProtocolSigner (e.g. remote signing).
overcastApiHTTP view over backendInject a custom OvercastView implementation.
axiosExtra axios defaults for the HTTP view.

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.

The surfaces

One app, six surfaces:

app.publicKey; // Id — your wallet public key / fee payer
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, mint); // escrow vault balance
await app.chain.getOption(collateralOfferId, settlementOfferId);
await app.chain.getCollateralOffer(id);
await app.chain.getSettlementOffer(id);
await app.chain.exerciseClaims(option, holder); // taker's remaining claims
await app.chain.redeemClaims(option, holder); // maker's remaining claims

Note that getOption takes the offer ID pair — on-chain, an option account is seeded by its two offers.

Writes (each builds, signs, and submits a transaction, returning TxInfo = { hash, confirmed }):

await app.chain.deposit({ depositor, token, amount });
await app.chain.withdraw({ withdrawer, token, amount });
await app.chain.createCollateralOffer(offer);
await app.chain.createSettlementOffer(offer);
await app.chain.acceptOffer(settlementOffer, collateralOffer); // mints the option
await app.chain.exerciseOption(option, amount); // taker
await app.chain.redeemOption(option, amount); // maker

app.api — curated backend views

Read-only, paginated queries against the backend's curated data (assets resolved, amounts formatted for display):

await app.api.listAssets(); // assets with curated metadata
await app.api.listOptions({ filter: { maker } });
await app.api.listOffers({ filter: { asset, status: "open" } }); // omit `status` for closed offers too
await app.api.listRfqs({ filter: { status: "open" } });
await app.api.listQuotes({ rfqId });

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 throws if the app has no signer ("RFQ requires a signer") because every quote and accept is authorized by an ed25519 signature.

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.