Task #3: MVP API — payment-gated jobs + demo endpoint

OpenAPI spec (lib/api-spec/openapi.yaml)
- Added POST /jobs, GET /jobs/{id}, GET /demo endpoints
- Added schemas: CreateJobRequest, CreateJobResponse, JobStatusResponse,
  InvoiceInfo, JobState, DemoResponse, ErrorResponse
- Ran codegen: generated CreateJobBody, GetJobResponse, RunDemoQueryParams etc.

Jobs router (artifacts/api-server/src/routes/jobs.ts)
- POST /jobs: validates body, creates LNbits eval invoice, inserts job +
  invoice in a DB transaction, returns { jobId, evalInvoice }
- GET /jobs/🆔 fetches job, calls advanceJob() helper, returns state-
  appropriate payload (eval/work invoice, reason, result, errorMessage)
- advanceJob() state machine:
  - awaiting_eval_payment: checks LNbits, atomically marks paid + advances
    state via optimistic WHERE state='awaiting_eval_payment'; runs
    AgentService.evaluateRequest, branches to awaiting_work_payment or rejected
  - awaiting_work_payment: same pattern for work invoice, runs
    AgentService.executeWork, advances to complete
  - Any agent/LNbits error transitions job to failed

Demo router (artifacts/api-server/src/routes/demo.ts)
- GET /demo?request=...: in-memory rate limiter (5 req/hour per IP)
- Explicit guard for missing request param (coerce.string() workaround)
- Calls AgentService.executeWork directly, returns { result }

Dev router (artifacts/api-server/src/routes/dev.ts)
- POST /dev/stub/pay/:paymentHash: marks stub invoice paid in-memory
- Only mounted when NODE_ENV !== 'production'

Route index updated to mount all three routers

replit.md: documented full curl flow with all 6 steps, demo endpoint,
and dev stub-pay trigger

End-to-end verified with curl:
- Full flow: create → eval pay → evaluating → work pay → executing → complete
- Error cases: 400 on missing body/param, 404 on unknown job
This commit is contained in:
alexpaynex
2026-03-18 15:31:26 +00:00
parent 9ec5e20a10
commit 4e8adbcb93
19 changed files with 1050 additions and 6 deletions

View File

@@ -14,3 +14,60 @@ import * as zod from "zod";
export const HealthCheckResponse = zod.object({
status: zod.string(),
});
/**
* Accepts a request, creates a job row, and issues an eval fee Lightning invoice.
* @summary Create a new agent job
*/
export const CreateJobBody = zod.object({
request: zod.string().min(1),
});
/**
* Returns current job state. Automatically advances the state machine when a pending invoice is found to be paid.
* @summary Get job status
*/
export const GetJobParams = zod.object({
id: zod.coerce.string(),
});
export const GetJobResponse = zod.object({
jobId: zod.string(),
state: zod.enum([
"awaiting_eval_payment",
"evaluating",
"rejected",
"awaiting_work_payment",
"executing",
"complete",
"failed",
]),
evalInvoice: zod
.object({
paymentRequest: zod.string(),
amountSats: zod.number(),
})
.optional(),
workInvoice: zod
.object({
paymentRequest: zod.string(),
amountSats: zod.number(),
})
.optional(),
reason: zod.string().optional(),
result: zod.string().optional(),
errorMessage: zod.string().optional(),
});
/**
* Runs the agent without payment. Limited to 5 requests per IP per hour.
* @summary Free demo (rate-limited)
*/
export const RunDemoQueryParams = zod.object({
request: zod.coerce.string(),
});
export const RunDemoResponse = zod.object({
result: zod.string(),
});