import type { McpTransport, SenpiClientConfig, CreatePositionRequest, CreatePositionResult, EditPositionResult, OrderStatus, GetPositionResult, OpenPosition, GetStrategyResult, GetOpenOrdersResult, GetTraderHistoryOptions, TraderHistoryEntry, GetTraderStateOptions, TraderStateResult, StrategyFundResult, CreateCustomStrategyParams, CreateCustomStrategyResult, PortfolioPayload, ClosePositionsRequest, GetPnlHistoryOptions, ListStrategiesFilters, StrategySummary, RatchetStopPosition, RatchetStopEvent, AddRatchetStopInput, EditRatchetStopInput, DeleteRatchetStopInput, QueryRatchetStopsInput, QueryRatchetStopEventsInput } from './types.js'; /** * Typed error thrown by `SenpiClient` mutation methods when the response * reports a failure at either layer (outer MCP envelope or inner engine * result). Callers that today only catch `throw` will fire on real * failures instead of silently treating phantom-success responses as * success — preventing the orphaned-DSL-state class of bug. * * Codes: * - `UNEXPECTED_RESPONSE` — response shape is not the expected object. * - `UPSTREAM_ERROR` — outer MCP envelope `success === false`. * - `ENGINE_FAILURE` — outer envelope ok, but inner engine result * reported `success === false` (e.g. CREATE_ORDER_RESTING). */ export declare class SenpiClientError extends Error { readonly code: "UNEXPECTED_RESPONSE" | "UPSTREAM_ERROR" | "ENGINE_FAILURE"; readonly details: unknown; /** * The venue may have acted despite the failure — set when the envelope reports * an ambiguous upstream code (the server's own execution-timeout abort) on a * mutating call. Callers must reconcile rather than re-send. */ readonly mayHaveExecuted: boolean; /** Envelope `error.code`, kept so telemetry and callers can key on it. */ readonly upstreamCode: string | undefined; constructor(code: "UNEXPECTED_RESPONSE" | "UPSTREAM_ERROR" | "ENGINE_FAILURE", message: string, details?: unknown, opts?: { mayHaveExecuted?: boolean; upstreamCode?: string; cause?: unknown; }); } /** JSON-RPC code the MCP SDK uses for a client-side request timeout. */ export declare const MCP_REQUEST_TIMEOUT_CODE = -32001; /** MCP request timeout for a call carrying an ALO execution budget. */ export declare function mcpTimeoutForAlo(opts?: { executionTimeoutSeconds?: number; }): number; /** * A `tools/call` the client stopped waiting for. **Not a failed order.** * * The server returns 200 on this work — an ALO order is *designed* to occupy the * connection for its full `execution_timeout_seconds`, so a timeout means we * stopped listening, not that nothing happened. Retrying a timed-out mutation * can double it (a second close against a closing position opens the reverse). * * `mayHaveExecuted` marks that ambiguity so callers reconcile instead of * retrying, and so the agent narrates a timeout rather than inventing an auth * fault from a bare `exception`. */ export declare class SenpiTimeoutError extends Error { /** * Numeric so `jsonRpcErrorCode` still resolves it: the span attribute * `rpc.response.status_code` and the `mcp.call_failed` event stay keyed on * `-32001`, which is what existing saved queries and alerts match on — and what * this class exists to make visible. */ readonly code = -32001; readonly toolName: string; readonly timeoutMs: number; /** Mutations may have landed server-side; reads cannot have side effects. */ readonly mayHaveExecuted: boolean; /** Safe to re-issue verbatim. False for mutations — reconcile instead. */ readonly retriable: boolean; constructor(toolName: string, timeoutMs: number, mayHaveExecuted: boolean, cause?: unknown); } /** * Whether a parsed envelope `error.code` leaves a mutation's outcome unknown. * * Exported so callers that get the code on a RETURNED result rather than a thrown error — * `createPosition` answers `success: false` with `errorCode` — classify it against the one * list here instead of re-deriving which codes mean "the venue may still have executed". */ export declare function isOutcomeUnknownCode(code: string | null | undefined): boolean; /** * Whether a thrown MCP failure is safe to re-issue verbatim. * * Two distinct paths reach here for the same underlying event — a slow order: * the client giving up (`SenpiTimeoutError`) and the *server* giving up at its * own 70s cap and returning `UNAVAILABLE`. Raising the client timeout above the * server's makes the second the common one, so guarding only the first would * move the duplicate-submission hazard rather than remove it. * * Exported so DSL and action call sites pass it to `withRetry`'s `shouldRetry` * rather than each re-deriving the rule. */ export declare function isRetriableMcpError(err: unknown): boolean; export declare class SenpiClient { private readonly injectedTransport; private readonly config; private mcpClient; private connectPromise; /** Live transport (self-managed mode) — read for the MCP span's session id. */ private mcpTransportRef; /** Static `network.transport`/`server.*` attrs for the MCP span, computed once at connect. */ private mcpServerAttrs; private readonly clearingHouseStaleness; private readonly staleRetryMaxAttempts; private readonly staleRetryBaseDelayMs; constructor(transportOrConfig: McpTransport | SenpiClientConfig); connect(): Promise; /** * Lazily (re)connect the self-managed client on demand (boot resilience). * * - Injected mode or already-connected → no-op. * - Otherwise connect once, de-duping concurrent callers onto a single in-flight * promise. On success `mcpClient` is set; on failure the in-flight promise is * cleared and the error is rethrown, so the NEXT call retries. This is what lets * the runtime start with MCP unreachable (the eager connect no longer has to * succeed) and resume the moment MCP returns — no process restart required. */ private ensureConnected; disconnect(): Promise; /** * Wrap each Senpi MCP call in an on-site `tools/call` span (`mcp.*` + `gen_ai.tool.*`). * `withSpan` uses `startActiveSpan`, so it nests under the active trading waterfall and * the auto-instrumented `POST /mcp` span nests under it; identity reuses the async-context * bag (same source as the logger and trading spans). */ private callTool; private callToolInner; /** * Run an MCP `audit_query` and return the unwrapped response payload. * * Stays a thin pass-through so the runtime layer (which owns the * wallet→strategyId resolution and error mapping) can compose the call * with full type-safety. Callers must already have set * `resource_type` / `resource_id` if scoping is desired — this method * does not interpret arguments. * * @param args Arbitrary key/value map forwarded verbatim as the MCP * tool input. Caller is responsible for matching the * MCP `audit_query` schema. * @param opts Optional AbortSignal; when fired, the underlying MCP * `callTool` is cancelled (the SDK rejects with an * AbortError). * @returns The `.data` field of the MCP envelope when present; * otherwise the raw response. */ auditQuery(args: Record, opts?: { signal?: AbortSignal; }): Promise; /** * Returns an adapter compatible with SenpiToolClient so the same connection * can be used for scanner provider MCP tool calls (e.g. leaderboard_get_markets). */ asToolClient(): { callTool(name: string, args: Record): Promise; }; /** * Register a backend ratchet-stop (DSL Phase 2 handoff). * * Throws `SenpiClientError` on outer envelope failure or inner engine * `success: false`. Callers' existing `if (result.success && result.position)` * branches now treat the success path only — failures land in the * surrounding try/catch instead of falling through. */ addRatchetStop(input: AddRatchetStopInput): Promise<{ success: boolean; position?: RatchetStopPosition; }>; /** * Update an existing backend ratchet-stop. * * Throws `SenpiClientError` on outer envelope failure or inner engine * `success: false`. */ editRatchetStop(input: EditRatchetStopInput): Promise<{ success: boolean; position?: RatchetStopPosition; }>; /** * Delete a backend ratchet-stop. Used during DSL teardown / handoff close. * * Throws `SenpiClientError` on outer envelope failure or inner engine * `success: false`. Previously this method's `{ success: boolean }` return * was ignored by every caller (only `throw` was caught), which left * zombie ratchet-stops on the backend when the engine reported failure. */ deleteRatchetStop(input: DeleteRatchetStopInput): Promise<{ success: boolean; }>; queryRatchetStops(input: QueryRatchetStopsInput): Promise; queryRatchetStopEvents(input: QueryRatchetStopEventsInput): Promise; private unwrapRatchetStopResult; createPosition(request: CreatePositionRequest): Promise; /** * Fetch market prices. Main DEX is passed as dex: ""; use dex: "xyz" for xyz. * If assets is omitted, the MCP returns all asset prices for that dex. */ getMarketPrices(params: { dex: string; assets?: string[]; }): Promise | null>; /** * Set/update SL (and optionally TP) on an open position. Uses MCP edit_position. * For XYZ DEX pass dex: "xyz" so coin is sent as "xyz:COIN"; otherwise main DEX uses plain coin. */ /** * Set / update an exchange-side stop loss (or perform other position * edits) on an open position. * * Throws `SenpiClientError` when either response layer reports failure: * - `UPSTREAM_ERROR` — MCP envelope `success: false`. * - `ENGINE_FAILURE` — outer ok, inner engine `success: false` * (e.g. position not found, SL placement rejected). * * Returning normally means the engine accepted the edit. The returned * `EditPositionResult` carries `slOrderId` when an SL was placed/updated; * a `null` slOrderId on a non-throwing return indicates the engine reported * success without the expected SL artifact — caller should treat that as * a soft failure (logged as a warn) and re-attempt on the next sync tick. */ editPosition(wallet: string, payload: { coin: string; dex?: string; stopLoss?: { triggerPx: number; }; }): Promise; getOrderStatus(wallet: string, orderId: number, opts?: { timeoutMs?: number; }): Promise; /** * Fetch full clearing house state for a wallet (main + xyz DEXes) from MCP. * * Retries with exponential backoff when the API returns stale data (timestamp * older than previously observed). By default, any other unreadable response * (thrown transport error, null/non-object unwrap, error envelope, or a shape * that does not look like clearing house state) is returned as null * immediately without retrying. * * Pass `retryUnreadable: true` (the trade/open path) to also retry those * unreadable forms, so an open is only blocked after the exchange view stays * unreadable across all attempts — never on a single transient failure. */ getClearingHouseState(wallet: string, opts?: { retryUnreadable?: boolean; signal?: AbortSignal; }): Promise | null>; /** * Single fetch + validate attempt. Returns a tagged result so the caller can * distinguish stale responses (always retryable) and unreadable responses * (retryable only on the trade path) from a clean read. * * When `retryUnreadable` is false (the default, non-trade callers), behavior * is unchanged from the stale-only design: a thrown transport error * propagates, and a response whose shape is not recognized is reported as * "ok" so downstream callers keep their own shape checks and distinct errors. * Only the trade path (`retryUnreadable` true) folds those forms into the * single retryable "unreadable" outcome. */ private fetchClearingHouseStateOnce; /** * Overridable delay for testing. Cut short (resolved, not rejected — the caller re-checks the * signal) when `signal` fires, so a retry backoff never outlives its caller's deadline. */ protected delay(ms: number, signal?: AbortSignal): Promise; /** * Fetch position (szi, entryPx, leverage) from clearing house for a wallet+coin. * Uses getClearingHouseState so one MCP call returns both main and xyz (and other) DEX positions; * then looks up the position for the given coin (and optional dex). */ getPosition(wallet: string, coin: string, dex?: string): Promise; /** * Resolve several assets from one clearing house fetch (single `strategy_get_clearinghouse_state` call). */ getPositionsForAssets(wallet: string, assets: string[], dex?: string): Promise>; closePosition(wallet: string, coin: string, params: { reason: string; dex?: string; orderType?: import("./types.js").CloseOrderType; feeOptimizedLimitOptions?: import("./types.js").FeeOptimizedLimitOptions; /** Structured close-reason context (notifications 2.3): the configured time limit in minutes (hard_timeout cuts). */ closeLimitMinutes?: number; /** Structured close-reason context (notifications 2.3): the peak ROE percent the cut decision used (weak_peak_cut). */ closePeakRoe?: number; }): Promise; /** * List open positions for reconciliation by parsing `strategy_get_clearinghouse_state` (same source as * {@link getPosition} / getPositionsForAssets). Does not call `list_open_positions`. * DEX keys `main` map to reconciler dex `""` to match DSL state paths; `xyz` stays `xyz`. */ listOpenPositions(wallet: string, opts?: { signal?: AbortSignal; }): Promise; /** * The strategy behind a wallet, or `null` when the backend lists none for it. * * `initialBudget` is `totalFunded` VERBATIM, including its `undefined`: an amount the backend did * not report is carried through as unread, never as `0`. This result is the strategy-details * surface (`StrategyState.fetchDetails` → the `strategy: details` context slice), and a `0` there * tells an agent the wallet is empty — the unreadable-renders-as-zero failure `[W_BUDGET_FUNDED_UNREADABLE]` * closes on the deploy report, one surface over. */ getStrategy(wallet: string): Promise; /** * `opts.signal` bounds ONE MCP call (H6). Deploy passes `AbortSignal.timeout(...)` on every call * it makes, so a backend that never answers cannot hold the deploy job's single-flight slot. * * FAILS CLOSED, like {@link listOpenPositions}: `callTool` throws only on an RPC fault, so a * tool-level error envelope (`{success:false,…}`) or an isError text payload RESOLVES here. Read * as `[]` those became "you own zero strategies" — and every caller treats zero as proof of * absence: deploy's reconcile falls through to `needsWallet`, so one transient backend failure * created and funded a second wallet beside every live one and then reported `live`. An empty * `strategies` array is still a real answer and is returned as one; anything else throws. */ listStrategies(filters?: ListStrategiesFilters, opts?: { signal?: AbortSignal; }): Promise; getOpenOrders(wallet: string): Promise; /** * Closed-position history from MCP `discovery_get_trader_history`. * Parsed `openTime` and `closedTime` are Unix epoch **seconds** (same as MCP `openTime` / `closeTime`). */ getTraderHistory(wallet: string, opts?: GetTraderHistoryOptions): Promise; topUp(wallet: string, amount: number): Promise; /** * Raw portfolio from account_get_portfolio. Interpretation (the funding * waterfall) lives in deploy/funding.ts — this method only fetches. * Returns null on a non-record payload; throws on transport error. */ getPortfolio(opts?: { forceFetch?: boolean; signal?: AbortSignal; }): Promise; /** * Create (and backend-fund) a custom strategy wallet. skillName/skillVersion * attribution is mandatory: strategies created without it are unattributable. */ createCustomStrategy(params: CreateCustomStrategyParams, opts?: { signal?: AbortSignal; }): Promise; withdrawFunds(wallet: string, amount: number): Promise; closeStrategy(wallet: string, opts?: { signal?: AbortSignal; }): Promise; /** * Submit a batch close_positions MCP call. * * Throws `SenpiClientError` on outer envelope or inner engine failure * (same shape as {@link closePosition}). Successful return means the * batch was accepted; per-position confirmation must still come from * position-tracker scanner events. */ closePositions(wallet: string, positions: ClosePositionsRequest[]): Promise>; /** * Cancel an outstanding order by id. * * Throws `SenpiClientError` on outer envelope or inner engine failure * (same shape as {@link closePosition}). */ cancelOrder(wallet: string, orderId: number): Promise>; getAssetTradingLimits(wallet: string, coin: string, dex?: string): Promise<{ maxLeverage: number; } | null>; listInstruments(): Promise>; /** * Non-delisted instrument NAMES from `market_list_instruments` — the live-universe gate's read. * Distinct from {@link listInstruments} on purpose: that projection requires `max_leverage` and * ignores `is_delisted`, both wrong for a liveness question (an instrument without a leverage * figure is still live; a delisted one is not). Returns [] when the response carries no * recognizable instruments — callers MUST treat an empty list as "could not read", never as * "everything is dead" (a real read always contains at least the majors). */ listInstrumentNames(opts?: { signal?: AbortSignal; }): Promise; /** * Fetch PnL and account value history plus the today_snapshot (UTC-day-scoped metrics) * via MCP `strategy_get_pnl_and_account_value_history`. * * Returns `history_by_period` (perpDay, perpWeek, perpMonth, perpAllTime) and a * `today_snapshot` with derived pnl, account_value, and drawdown metrics for the current * UTC calendar day. today_snapshot is absent if perpDay data is unavailable. * * @param wallet - Strategy wallet address (42-char hex). * @param opts.drawdownResetOnDayRollover - When false, PnL peak carries from pre-midnight (~24h). * Default true resets at UTC midnight (intraday drawdown only). */ getPnlHistoryWithSnapshot(wallet: string, opts?: GetPnlHistoryOptions): Promise; /** * Fetch current state for a single wallet via MCP `discovery_get_trader_state`. * * When opts.includePositionAge is true, each open position includes startTime * (Unix seconds, ClickHouse) and durationInSeconds. Required for Gate 5 * (max entries/day) to count positions opened today. * * Returns null when the MCP response is missing or the trader is not found. * Callers implementing fail-closed gates must treat null as a CLOSED signal. */ getTraderState(wallet: string, opts?: GetTraderStateOptions): Promise; } /** * True if the payload looks like Senpi/Hyperliquid clearing house (so an empty list means no positions, * not an unrelated JSON object). */ export declare function clearingHouseStateLooksParsable(state: Record): boolean; /** * Fill detail of a close, parsed from an (unwrapped) `close_position` response. The engine's * `ClosePositionResult` is flat: `{ success, orderId, closedPrice, closedSize, executionAsMaker, * ... }` (see hyperliquid_mcp `senpi.types.ts`). `orderId` is the venue (Hyperliquid) order id of * the closing order — the join key into the fills ledger for post-hoc lifecycle correlation. */ export interface CloseFillDetail { orderId?: string; closedPrice?: number; closedSize?: number; /** True = maker, false = taker, null = unknown (engine reported null). */ executionAsMaker?: boolean | null; } /** * Best-effort: reads the flat engine shape, with a `data`-nested fallback in case a caller hands * the still-enveloped response. Missing/malformed fields are simply absent — callers stamp the * detail opportunistically on telemetry emits and never branch on it. */ export declare function parseCloseFillDetail(payload: unknown): CloseFillDetail; //# sourceMappingURL=client.d.ts.map