@discomedia/papertiger
v0.1.11
Published
Official type-safe TypeScript client for PaperTiger paper trading and historical prediction-market data.
Maintainers
Readme
@discomedia/papertiger
Official type-safe TypeScript client for the PaperTiger paper-trading API.
PaperTiger simulates orders against public Polymarket and Kalshi order books. Limit fills conservatively attribute at most 30% of each executable displayed level to the paper order and prevent an unchanged GTC book from being consumed more than once per one-minute observation window. It never submits a real-money order.
Install
Use the package manager already used by your project:
pnpm add @discomedia/papertigernpm install @discomedia/papertigerThe package has no runtime dependencies and includes ESM, CommonJS, TypeScript declarations, the complete API reference, and LLM-oriented discovery documentation.
Quick start
Create an API key in the PaperTiger dashboard, then keep it in a server-side environment variable:
export PAPERTIGER_API_KEY="pt_key_replace_me"import { PaperTigerClient } from "@discomedia/papertiger";
const apiKey = process.env.PAPERTIGER_API_KEY;
if (apiKey === undefined) {
throw new Error("PAPERTIGER_API_KEY is required.");
}
const paperTiger = new PaperTigerClient({
apiKey,
});
const wallets = await paperTiger.listWallets();
const { markets, warnings } = await paperTiger.searchMarkets({
q: "bitcoin",
venue: "all",
});
console.log({ wallets, markets, warnings });New keys initially access only the wallet selected when they are created.
Wallet-scoped calls can omit walletId and walletName to use the editable
default wallet.
Do not embed API keys in browser bundles. Browser applications can use the
session returned by login() or call PaperTiger through their own server.
New accounts must explicitly acknowledge PaperTiger's 13+ eligibility rule and the parental or guardian permission requirement that applies to users under their local age of majority:
await paperTiger.register({
email: "[email protected]",
password: "a-long-unique-password",
eligibilityAcknowledged: true,
});Place a retry-safe paper order
Use canonical market and outcome identifiers returned by market search:
const market = markets.at(0);
const outcome = market?.outcomes.at(0);
if (market === undefined || outcome === undefined) {
throw new Error("A tradable market outcome is required.");
}
const order = await paperTiger.placeOrder(
{
venue: market.venue,
marketId: market.marketId,
outcomeId: outcome.id,
side: "buy",
quantity: 10,
type: "limit",
timeInForce: "gtc",
limitPricePercent1: 0.54,
executionFloorPricePercent1: 0.5,
clientOrderId: "strategy-a-2026-07-27-001",
},
{
idempotencyKey: crypto.randomUUID(),
},
);
console.log(order.status);Reuse an idempotencyKey only when retrying the identical order request.
clientOrderId is separately wallet-unique and is intended for reconciling
your strategy state.
For a buy execution band, set executionFloorPricePercent1 alongside a
limit. For example, a 0.90 floor and 0.98 buy limit accept only consumed
book levels from 90c through 98c. This is enforced during the execution
engine's fill walk, including each later GTC evaluation—not by an advisory
client-side book check. Floors are buy-only and cannot exceed the limit.
Use maxSpendDollars instead of quantity to cap a buy at an all-in dollar
budget, including the simulated venue fee:
const dollarSizedOrder = await paperTiger.placeOrder({
venue: market.venue,
marketId: market.marketId,
outcomeId: outcome.id,
side: "buy",
maxSpendDollars: 100,
type: "market",
maxSlippagePercent100: 4,
});Bulk order actions
bulkOrders() sends 1–50 place, replace, and cancel actions in one request.
Actions run in order and return independent success or failure results:
const bulkResult = await paperTiger.bulkOrders([
{
action: "place",
idempotencyKey: crypto.randomUUID(),
order: {
venue: market.venue,
marketId: market.marketId,
outcomeId: outcome.id,
side: "buy",
maxSpendDollars: 50,
type: "limit",
timeInForce: "gtc",
limitPricePercent1: 0.48,
},
},
{
action: "cancel",
orderId: "existing-order-id",
},
]);
for (const result of bulkResult.results) {
if (result.status === "failed") {
console.error(result.index, result.error.code, result.error.message);
}
}The server validates the entire bulk payload before executing it. Execution is best-effort rather than atomic, so one item failure does not roll back earlier items or prevent later items.
Retrieve trading history by time
Order, position, and account-activity list methods accept inclusive ISO 8601
start and end bounds. Orders are bounded by creation time, aggregate
positions by their original creation time, and activities by occurrence time:
const start = "2026-07-01T00:00:00.000Z";
const end = "2026-07-31T23:59:59.999Z";
const [{ orders }, { positions }, { activities }] = await Promise.all([
paperTiger.listOrders({ start, end }),
paperTiger.listPositions("wallet-id", { status: "all", start, end }),
paperTiger.listActivities("wallet-id", { start, end }),
]);Keep the same bounds and other filters when following an order or activity
nextCursor.
Query historical market data
Historical pages use stable cursors and include provenance and coverage
quality. Market-level coverage start/end values apply to the row's highest
reported fidelity, so use replayable_l2 rows when selecting an executable
backtest window. Market rows include title, inferred underlying/duration, and
scheduled close metadata so callers can select coverage for an exact rolling
family. Coverage may also report collectionMode: "sampled" and a
scheduledNextCaptureAt value during cost-bounded debug collection; do not
treat separate sampled intervals as continuous. Keep all filters unchanged when
following nextCursor:
const coverage = await paperTiger.getMarketDataCoverage();
const trades = await paperTiger.listHistoricalTrades({
venue: "polymarket",
marketId: "venue-market-id",
start: "2026-07-01T00:00:00.000Z",
end: "2026-07-28T00:00:00.000Z",
limit: 10_000,
});
const book = await paperTiger.getHistoricalOrderBook(
"polymarket",
"venue-market-id",
"2026-07-28T00:00:00.000Z",
"venue-outcome-id",
);
console.log({ coverage, trades: trades.data.length, bids: book.bids });The market-data client methods are:
| Method | Data |
| --- | --- |
| getMarketDataCoverage() | Ranges, fidelity, quality, freshness, and known gaps |
| listHistoricalMarkets() | Current or point-in-time definitions, optionally bounded by scheduled close time |
| getHistoricalMarket() | One current or point-in-time definition |
| listHistoricalTrades() | Exact normalized public trades |
| listHistoricalQuotes() | Change-only best bid/ask observations |
| listHistoricalCandles() | One-minute price, quote, volume, and open-interest candles |
| getHistoricalOrderBook() | Causally reconstructed replayable L2 depth |
| createMarketDataExport() | Start a bounded Parquet export |
| getMarketDataExport() | Read export status and obtain a temporary download URL |
Generate and backtest a strategy
generateStrategy() uses PaperTiger's server-side gpt-5.6-luna
configuration to turn a natural-language idea into an editable, validated
version-two weighted L2 strategy. It also returns provider token and cost usage:
const generated = await paperTiger.generateStrategy(
"Buy Polymarket BTC 5-minute Up only when the weighted L2 score persists for 750ms, Chainlink agrees, both books are fresh, ephemeral liquidity stays low, and executable net edge after fees and slippage is at least 0.25%.",
);
console.log(generated.strategy, generated.usage);V2 strategies support hard entry safeguards in addition to weighted signals: score persistence, reference-direction agreement, maximum book age, ephemeral-liquidity abstention, and an extra expected-edge slippage buffer. Known collection gaps always fail closed.
Review the definition, then run a bounded point-in-time simulation:
const result = await paperTiger.runBacktest({
strategy: generated.strategy,
start: "2026-07-27T00:00:00.000Z",
end: "2026-07-28T00:00:00.000Z",
initialCapitalDollars: 1_000,
});
console.log({
returnPercent100: result.metrics.returnPercent100,
maxDrawdownPercent100: result.metrics.maxDrawdownPercent100,
trades: result.trades.length,
equityPoints: result.equityCurve.length,
retainedDecisions: result.decisions?.length ?? 0,
firstTradeDiagnostics: result.trades[0]?.diagnostics,
});Interactive runs span at most 31 days and 500,000 market-data events. V1
remains accepted. V2 processes full snapshots, absolute-size deltas, and public
trades only after observedAt; maintains both outcome books; walks asks and
bids after configured latency; applies per-level depth participation, slippage,
and venue fee curves; and fails closed across known gaps. Each completed trade
includes score components, executable book and flow state, expected net edge,
rejection context, and 250 ms/1 s/3 s/10 s executable-bid markouts. Every run
persists its immutable strategy version and frozen data watermarks.
Track experiments and forward paper runs
Use the experiment control plane when a hypothesis must remain reproducible across strategy revisions and historical or forward evaluations:
const experiment = await paperTiger.createExperiment({
name: "BTC five-minute depth pressure",
hypothesis: "Positive executable depth imbalance predicts the Up outcome.",
successCriteria: { minimumCompletedTrades: 30 },
guardrails: { paperOnly: true },
});
const version = await paperTiger.createExperimentStrategyVersion(
experiment.id,
{
definition: generated.strategy,
changeNote: "Initial causal V2 definition.",
engineVersion: "backtest-l2-v2",
},
);
const historical = await paperTiger.runExperimentBacktest(experiment.id, {
strategyVersionId: version.versionId,
start: "2026-07-27T00:00:00.000Z",
end: "2026-07-28T00:00:00.000Z",
initialCapitalDollars: 1_000,
});
const analysis = await paperTiger.analyzeExperimentRun(historical.run.id);
const bundle = await paperTiger.exportExperimentResearchBundle(
historical.run.id,
);Managed forward-paper runs use a dedicated wallet. Externally hosted bots use
createExperimentRun() with mode: "external_paper" and submit retry-safe
causal decisions through ingestExperimentDecisions(). Use
compareExperimentRuns() only for compatible universes, windows, capital, and
execution assumptions.
To page without changing the query snapshot:
const query = {
venue: "polymarket" as const,
marketId: "venue-market-id",
start: "2026-07-01T00:00:00.000Z",
end: "2026-07-28T00:00:00.000Z",
limit: 1_000,
};
const firstPage = await paperTiger.listHistoricalQuotes(query);
const secondPage =
firstPage.nextCursor === null
? null
: await paperTiger.listHistoricalQuotes({
...query,
cursor: firstPage.nextCursor,
});getHistoricalOrderBook() returns only causally replayable depth. Gapped or
unavailable depth throws PaperTigerApiError with code
INSUFFICIENT_MARKET_DATA; trades and proxy candles are never substituted.
Create a bounded Parquet export and poll it until complete:
const exportJob = await paperTiger.createMarketDataExport({
dataset: "trades",
start: "2026-07-01T00:00:00.000Z",
end: "2026-07-28T00:00:00.000Z",
});
const currentExport = await paperTiger.getMarketDataExport(exportJob.id);
console.log(currentExport.status, currentExport.downloadUrl);Exports may cover at most 90 days and expire after 24 hours. A completed job
returns a private downloadUrl valid for 15 minutes. Fetch that URL directly;
do not send the PaperTiger API key to the storage host.
Errors
Non-successful HTTP responses throw PaperTigerApiError:
import {
PaperTigerApiError,
PaperTigerClient,
} from "@discomedia/papertiger";
try {
await paperTiger.getPortfolio("wallet-id");
} catch (error: unknown) {
if (error instanceof PaperTigerApiError) {
console.error({
status: error.status,
code: error.code,
requestId: error.requestId,
details: error.details,
});
}
}Network errors and caller-triggered aborts remain standard Fetch API errors. The client does not automatically retry trading mutations.
Authentication
Server integrations should normally construct the client with apiKey.
Account applications can use sessionToken. login() and confirmEmail()
automatically activate the returned session on that client instance:
const accountClient = new PaperTigerClient();
const { user, sessionToken } = await accountClient.login({
email: "[email protected]",
password: "replace-with-a-secure-password",
});The package never persists credentials. Store sessionToken using the secure
storage appropriate to your runtime and pass it back to a new client when
needed.
API coverage
PaperTigerClient covers health, contact, account authentication, wallets,
portfolio history, activity, market discovery, URL resolution, order books,
historical definitions/trades/quotes/candles/L2 replay/Parquet exports, single
and bulk orders, fills, positions, strategy generation, historical backtests,
experiments, immutable strategy versions, managed/external paper runs,
decision ingestion, deterministic run analysis/comparison/export, and API-key
management. Every method accepts an optional AbortSignal.
Account clients can call updateApiKeyDefaultWallet() to change the wallet
used when a key-authenticated request omits a wallet selector, and
updateApiKeyAccess() to switch between default-wallet-only and all-wallet
access. updateApiKeyLevel() grants or removes account-management authority,
and updateApiKey() can change the settings together. Account-level keys are
always forced to all-wallet access.
Create an account-level key, then use it to manage paper accounts:
const created = await accountClient.createApiKey({
name: "Account operator",
walletId: "existing-wallet-id",
level: "account",
});
const accountApi = new PaperTigerClient({ apiKey: created.key });
const paperAccount = await accountApi.createAccount({
name: "Research",
startingBalanceDollars: 10_000,
});
await accountApi.updateAccount(paperAccount.id, { name: "Research v2" });
await accountApi.resetAccount(paperAccount.id);
await accountApi.deleteAccount(paperAccount.id);deleteAccount() permanently removes all trading history. The last paper
account cannot be deleted, and an active API key's default account must be
changed or revoked before that account can be destroyed.
The public request<Response>() method supports newly released endpoints
before a dedicated convenience method is available.
Included documentation
- Complete API reference: package export
@discomedia/papertiger/api.md - Historical market-data guide: package export
@discomedia/papertiger/market-data.md - LLM discovery guide: package export
@discomedia/papertiger/llms.txt - Rendered current documentation: papertigerapp.com/docs
- Canonical Markdown: papertigerapp.com/api.md
The package build copies the canonical API and LLM files into the published tarball, so installed-package documentation cannot silently diverge from the website source.
