Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions packages/monitor-v2/src/monitor-polymarket/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getRetryProvider, paginatedEventQuery as umaPaginatedEventQuery } from "@uma/common";
import { createHttpClient } from "@uma/toolkit";
import { AxiosError, AxiosInstance } from "axios";
import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
export const paginatedEventQuery = umaPaginatedEventQuery;

import type { Provider } from "@ethersproject/abstract-provider";
Expand Down Expand Up @@ -75,10 +75,12 @@ export interface MonitoringParams {
fillEventsLookbackSeconds: number;
fillEventsProposalGapSeconds: number;
httpClient: ReturnType<typeof createHttpClient>;
aiDeeplinkHttpClient: ReturnType<typeof createHttpClient>;
orderBookBatchSize: number;
ooV2Addresses: string[];
ooV1Addresses: string[];
aiConfig?: AIConfig;
aiDeeplinkTimeout: number;
}
interface PolymarketMarketGraphql {
question: string;
Expand Down Expand Up @@ -712,16 +714,25 @@ export async function fetchLatestAIDeepLink(
if (!params.aiConfig) {
return { deeplink: undefined };
}
const startTime = Date.now();
try {
const questionId = calculatePolymarketQuestionID(proposal.ancillaryData);
const response = await params.httpClient.get<UMAAIRetriesLatestResponse>(params.aiConfig.apiUrl, {
const requestConfig: AxiosRequestConfig = {
params: {
limit: 50,
search: proposal.proposalHash,
last_page: false,
project_id: params.aiConfig.projectId,
},
});
};

requestConfig.timeout = params.aiDeeplinkTimeout;

const response = await params.aiDeeplinkHttpClient.get<UMAAIRetriesLatestResponse>(
params.aiConfig.apiUrl,
requestConfig
);
const duration = Date.now() - startTime;

const result = response.data?.elements?.find(
(element) => element.data.input.timing?.expiration_timestamp === proposal.proposalExpirationTimestamp.toNumber()
Expand All @@ -739,20 +750,52 @@ export async function fetchLatestAIDeepLink(
status: response.status,
statusText: response.statusText,
},
durationMs: duration,
notificationPath: "otb-monitoring",
});
return { deeplink: undefined };
}

logger.debug({
at: "PolymarketMonitor",
message: "Successfully fetched AI deeplink",
proposalHash: proposal.proposalHash,
durationMs: duration,
});

return {
deeplink: `${params.aiConfig.resultsBaseUrl}/${result.id}`,
};
} catch (error) {
const duration = Date.now() - startTime;
const axiosError = error as AxiosError;

logger.debug({
at: "PolymarketMonitor",
message: "Failed to fetch AI deeplink",
err: error instanceof Error ? error.message : String(error),
proposalHash: proposal.proposalHash,
durationMs: duration,
errorDetails: {
code: axiosError?.code,
response: axiosError?.response
? {
status: axiosError.response?.status,
statusText: axiosError.response?.statusText,
headers: axiosError.response?.headers,
}
: undefined,
request: axiosError?.config
? {
url: axiosError.config?.url,
method: axiosError.config?.method,
timeout: axiosError.config?.timeout,
baseURL: axiosError.config?.baseURL,
}
: undefined,
isTimeout:
axiosError?.code === "ECONNABORTED" || (error instanceof Error && error.message?.includes("timeout")),
},
});
return { deeplink: undefined };
}
Expand Down Expand Up @@ -902,6 +945,7 @@ export const initMonitoringParams = async (
const minTimeBetweenRequests = env.MIN_TIME_BETWEEN_REQUESTS ? Number(env.MIN_TIME_BETWEEN_REQUESTS) : 200;

const httpTimeout = env.HTTP_TIMEOUT ? Number(env.HTTP_TIMEOUT) : 10_000;
const aiDeeplinkTimeout = env.AI_DEEPLINK_TIMEOUT ? Number(env.AI_DEEPLINK_TIMEOUT) : 10_000;

const shouldResetTimeout = env.SHOULD_RESET_TIMEOUT !== "false";

Expand All @@ -924,6 +968,27 @@ export const initMonitoringParams = async (
},
});

// Create a separate HTTP client for AI deeplink requests with unlimited concurrency
// This prevents AI deeplink requests from being queued behind other rate-limited requests
const aiDeeplinkHttpClient = createHttpClient({
axios: { timeout: aiDeeplinkTimeout },
rateLimit: { maxConcurrent: null, minTime: 0 }, // No rate limiting - unlimited concurrency
retry: {
retries: retryAttempts,
baseDelayMs: retryDelayMs,
shouldResetTimeout: false, // Don't reset timeout on retries - keep total time bounded by single timeout + retry delays
onRetry: (retryCount, err, config) => {
logger.debug({
at: "PolymarketMonitor",
message: `ai-deeplink-retry attempt #${retryCount} for ${config?.url}`,
error: err.code || err.message,
retryCount,
timeout: config?.timeout,
});
},
},
});

const ooV2Addresses = parseEnvList(env, "OOV2_ADDRESSES", [await getAddress("OptimisticOracleV2", chainId)]);
const ooV1Addresses = parseEnvList(env, "OOV1_ADDRESSES", [await getAddress("OptimisticOracle", chainId)]);

Expand All @@ -947,10 +1012,12 @@ export const initMonitoringParams = async (
fillEventsLookbackSeconds,
fillEventsProposalGapSeconds,
httpClient,
aiDeeplinkHttpClient,
orderBookBatchSize,
ooV2Addresses,
ooV1Addresses,
aiConfig,
aiDeeplinkTimeout,
};
};

Expand Down
15 changes: 9 additions & 6 deletions packages/toolkit/src/http/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Bottleneck from "bottleneck";
import axiosRetry, { IAxiosRetryConfig } from "axios-retry";

export interface RateLimitOptions {
/** Max requests running in parallel (default = 5) */
maxConcurrent?: number;
/** Max requests running in parallel (default = 5). Set to null for unlimited concurrency. */
maxConcurrent?: number | null;
/** Minimum gap in ms between jobs (default = 200 → ≈5 req/s) */
minTime?: number;
}
Expand Down Expand Up @@ -47,15 +47,18 @@ export interface HttpClientOptions {
* @returns An Axios instance
*/
export function createHttpClient(opts: HttpClientOptions = {}): AxiosInstance {
const { maxConcurrent = 5, minTime = 200 } = opts.rateLimit ?? {};
const limiter = new Bottleneck({ maxConcurrent, minTime });

const instance = axios.create({
timeout: 10_000, // default timeout of 10 seconds
...opts.axios,
});

instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg));
// Only use Bottleneck if maxConcurrent is not null (null means unlimited, skip rate limiting entirely)
const maxConcurrent = opts.rateLimit?.maxConcurrent ?? 5;
if (maxConcurrent !== null) {
const minTime = opts.rateLimit?.minTime ?? 200;
const limiter = new Bottleneck({ maxConcurrent, minTime });
instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg));
}

const { retries = 3, retryCondition, onRetry, baseDelayMs = 100, maxJitterMs = 1000, maxDelayMs = 10_000 } =
opts.retry ?? {};
Expand Down