Skip to content

Instantly share code, notes, and snippets.

@worm-emoji
Created February 19, 2026 19:42
Show Gist options
  • Select an option

  • Save worm-emoji/794c13433a7b830e916f90d7808ce3f9 to your computer and use it in GitHub Desktop.

Select an option

Save worm-emoji/794c13433a7b830e916f90d7808ce3f9 to your computer and use it in GitHub Desktop.
pmapi: local public v2 question-gen + create-market test script
/**
* Test script for public v2 question generation + market creation.
*
* Usage:
* bun run apps/pmapi/scripts/test-public-create-market.ts --question "Will ... ?"
* bun run apps/pmapi/scripts/test-public-create-market.ts --questionId <0xquestionId>
*
* Optional flags:
* --apiBase API base URL (default: http://localhost:3001/public/v2)
* --apiKey Bearer token (default: local)
* --pollMs Poll interval in ms when using --question (default: 2000)
* --maxAttempts Max polling attempts when using --question (default: 45)
*/
import { parseArgs } from 'node:util'
import { z } from 'zod'
import { log } from '../src/common/log'
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
question: { type: 'string', short: 'q' },
questionId: { type: 'string', short: 'i' },
apiBase: { type: 'string', short: 'b' },
apiKey: { type: 'string', short: 'k' },
pollMs: { type: 'string' },
maxAttempts: { type: 'string' },
help: { type: 'boolean', short: 'h' },
},
})
const question = values.question ?? Bun.env.QUESTION
const questionId = values.questionId ?? Bun.env.QUESTION_ID
const apiBase = values.apiBase ?? 'http://localhost:3001/public/v2'
const apiKey = values.apiKey ?? Bun.env.API_KEY ?? 'local'
const pollMs = Number(values.pollMs ?? Bun.env.POLL_MS ?? '2000')
const maxAttempts = Number(values.maxAttempts ?? Bun.env.MAX_ATTEMPTS ?? '45')
const apiOrigin = new URL(apiBase).origin
const normalizedApiBase = apiBase.endsWith('/') ? apiBase.slice(0, -1) : apiBase
const zSubmitQuestionResponse = z.object({
submissionId: z.string(),
pollUrl: z.string().optional(),
status: z.string(),
})
const zQuestionSubmissionResponse = z.object({
submissionId: z.string().optional(),
status: z.enum(['pending', 'processing', 'completed', 'failed']),
questions: z.array(
z.object({
id: z.string(),
text: z.string().optional(),
criteria: z.string().optional(),
}),
),
statusUpdates: z
.array(
z.object({
tool: z.string(),
status: z.string(),
timestamp: z.string(),
}),
)
.default([]),
})
const zCreateMarketResponse = z.object({
marketId: z.string(),
txHash: z.string(),
})
const printUsage = () => {
log.info('Usage:')
log.info(
' bun run apps/pmapi/scripts/test-public-create-market.ts --question "Will ... ?"',
)
log.info(
' bun run apps/pmapi/scripts/test-public-create-market.ts --questionId <0xquestionId>',
)
log.info(
'Options: --apiBase <url> --apiKey <token> --pollMs <ms> --maxAttempts <n>',
)
log.info(
'Env fallback: QUESTION, QUESTION_ID, API_KEY, POLL_MS, MAX_ATTEMPTS',
)
}
if (values.help) {
printUsage()
process.exit(0)
}
if (!questionId && !question) {
printUsage()
process.exit(1)
}
if (!Number.isFinite(pollMs) || pollMs <= 0) {
log.error('pollMs must be a positive number')
process.exit(1)
}
if (!Number.isFinite(maxAttempts) || maxAttempts <= 0) {
log.error('maxAttempts must be a positive number')
process.exit(1)
}
const resolveUrl = (path: string) => {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path
}
if (path.startsWith('/')) {
return `${apiOrigin}${path}`
}
return `${normalizedApiBase}/${path}`
}
const apiRequest = async (path: string, init: RequestInit = {}) => {
const response = await fetch(resolveUrl(path), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
...init.headers,
},
...init,
})
const raw = await response.text()
const data = (() => {
try {
return JSON.parse(raw) as unknown
} catch {
return raw
}
})()
return { status: response.status, data }
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const createMarket = async (id: string) => {
log.info('Creating market from questionId')
log.info(`questionId: ${id}`)
const response = await apiRequest('markets/create', {
method: 'POST',
body: JSON.stringify({ questionId: id }),
})
log.info(`create status: ${response.status}`)
log.info('create response:')
log.info(JSON.stringify(response.data, null, 2))
if (!response.status || response.status !== 200) {
process.exit(1)
}
const parsed = zCreateMarketResponse.safeParse(response.data)
if (!parsed.success) {
log.error(
{ error: parsed.error.flatten() },
'Invalid create response shape',
)
process.exit(1)
}
}
const runQuestionFlow = async (prompt: string) => {
log.info('Submitting question')
log.info(`question: ${prompt}`)
const submitResponse = await apiRequest('questions', {
method: 'POST',
body: JSON.stringify({ question: prompt }),
})
log.info(`submit status: ${submitResponse.status}`)
log.info('submit response:')
log.info(JSON.stringify(submitResponse.data, null, 2))
if (submitResponse.status !== 200) {
process.exit(1)
}
const submitParsed = zSubmitQuestionResponse.safeParse(submitResponse.data)
if (!submitParsed.success) {
log.error(
{ error: submitParsed.error.flatten() },
'Invalid submit response shape',
)
process.exit(1)
}
const pollTarget =
submitParsed.data.pollUrl ??
`questions/submissions/${submitParsed.data.submissionId}`
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const pollResponse = await apiRequest(pollTarget, { method: 'GET' })
if (pollResponse.status !== 200) {
log.error({ pollResponse }, 'Polling failed')
process.exit(1)
}
const pollParsed = zQuestionSubmissionResponse.safeParse(pollResponse.data)
if (!pollParsed.success) {
log.error(
{ error: pollParsed.error.flatten() },
'Invalid polling response shape',
)
process.exit(1)
}
const latestUpdate =
pollParsed.data.statusUpdates[pollParsed.data.statusUpdates.length - 1]
const updateSummary = latestUpdate
? ` | ${latestUpdate.tool}: ${latestUpdate.status}`
: ''
log.info(
`poll ${attempt}/${maxAttempts}: status=${pollParsed.data.status}${updateSummary}`,
)
if (pollParsed.data.status === 'failed') {
log.error({ pollResponse: pollParsed.data }, 'Question generation failed')
process.exit(1)
}
if (pollParsed.data.status === 'completed') {
const firstQuestion = pollParsed.data.questions[0]
if (!firstQuestion) {
log.error('Question generation completed with no questions')
process.exit(1)
}
if (firstQuestion.text) {
log.info(`selected question: ${firstQuestion.text}`)
}
if (firstQuestion.criteria) {
log.info(`criteria: ${firstQuestion.criteria}`)
}
await createMarket(firstQuestion.id)
return
}
await sleep(pollMs)
}
log.error(
`Question generation did not complete within ${maxAttempts} attempts`,
)
process.exit(1)
}
const main = async () => {
const redactedApiKey =
apiKey.length <= 6 ? '***' : `${apiKey.slice(0, 3)}...${apiKey.slice(-2)}`
log.info(`apiBase: ${apiBase}`)
log.info(`auth: Bearer ${redactedApiKey}`)
if (questionId) {
await createMarket(questionId)
return
}
if (question) {
await runQuestionFlow(question)
}
}
main().catch((error) => {
log.error({ error }, 'Script failed')
process.exit(1)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment