Add session mode for pre-funded request processing

Implement session-based API endpoints for creating, managing, and interacting with pre-funded sessions, including deposit and top-up invoice generation, macaroon authentication, and per-request debiting of compute costs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2dc3847e-7186-4a22-9c7e-16cd31bca8d9
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-18 20:00:24 +00:00
parent dfc9ecdc7b
commit ab2cc06a79
29 changed files with 1075 additions and 978 deletions

View File

@@ -1,124 +0,0 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Api
* API specification
* OpenAPI spec version: 0.1.0
*/
export interface HealthStatus {
status: string;
}
export interface ErrorResponse {
error: string;
}
export interface InvoiceInfo {
paymentRequest: string;
amountSats: number;
}
export interface CreateJobRequest {
/** @minLength 1 */
request: string;
}
export interface CreateJobResponse {
jobId: string;
evalInvoice: InvoiceInfo;
}
export type JobState = (typeof JobState)[keyof typeof JobState];
export const JobState = {
awaiting_eval_payment: "awaiting_eval_payment",
evaluating: "evaluating",
rejected: "rejected",
awaiting_work_payment: "awaiting_work_payment",
executing: "executing",
complete: "complete",
failed: "failed",
} as const;
/**
* Cost breakdown shown with the work invoice (estimations at invoice-creation time)
*/
export interface PricingBreakdown {
/** Total estimated cost in USD (token cost + DO infra + margin) */
estimatedCostUsd?: number;
/** Originator margin percentage applied */
marginPct?: number;
/** BTC/USD spot price used to convert the invoice to sats */
btcPriceUsd?: number;
}
/**
* Lifecycle of the refund for this job
*/
export type CostLedgerRefundState =
(typeof CostLedgerRefundState)[keyof typeof CostLedgerRefundState];
export const CostLedgerRefundState = {
not_applicable: "not_applicable",
pending: "pending",
paid: "paid",
} as const;
/**
* Honest post-work accounting stored after the job completes
*/
export interface CostLedger {
actualInputTokens?: number;
actualOutputTokens?: number;
/** Sum of actualInputTokens + actualOutputTokens */
totalTokens?: number;
/** Raw Anthropic token cost (no infra, no margin) */
actualCostUsd?: number;
/** What we honestly charged in USD (actual token cost + DO infra + margin) */
actualChargeUsd?: number;
/** Original estimate used to create the work invoice */
estimatedCostUsd?: number;
/** Honest sats charge (actual cost converted at the locked BTC price) */
actualAmountSats?: number;
/** Amount the user originally paid in sats */
workAmountSats?: number;
/** Sats owed back to the user (workAmountSats - actualAmountSats, >= 0) */
refundAmountSats?: number;
/** Lifecycle of the refund for this job */
refundState?: CostLedgerRefundState;
marginPct?: number;
/** BTC/USD price locked at invoice creation time */
btcPriceUsd?: number;
}
export interface JobStatusResponse {
jobId: string;
state: JobState;
evalInvoice?: InvoiceInfo;
workInvoice?: InvoiceInfo;
pricingBreakdown?: PricingBreakdown;
reason?: string;
result?: string;
costLedger?: CostLedger;
errorMessage?: string;
}
export interface ClaimRefundRequest {
/** BOLT11 invoice for exactly refundAmountSats */
invoice: string;
}
export interface ClaimRefundResponse {
ok: boolean;
refundAmountSats: number;
paymentHash: string;
message: string;
}
export interface DemoResponse {
result: string;
}
export type RunDemoParams = {
request: string;
};

View File

@@ -1,456 +0,0 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Api
* API specification
* OpenAPI spec version: 0.1.0
*/
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
MutationFunction,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type {
ClaimRefundRequest,
ClaimRefundResponse,
CreateJobRequest,
CreateJobResponse,
DemoResponse,
ErrorResponse,
HealthStatus,
JobStatusResponse,
RunDemoParams,
} from "./api.schemas";
import { customFetch } from "../custom-fetch";
import type { ErrorType, BodyType } from "../custom-fetch";
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Returns server health status
* @summary Health check
*/
export const getHealthCheckUrl = () => {
return `/api/healthz`;
};
export const healthCheck = async (
options?: RequestInit,
): Promise<HealthStatus> => {
return customFetch<HealthStatus>(getHealthCheckUrl(), {
...options,
method: "GET",
});
};
export const getHealthCheckQueryKey = () => {
return [`/api/healthz`] as const;
};
export const getHealthCheckQueryOptions = <
TData = Awaited<ReturnType<typeof healthCheck>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof healthCheck>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getHealthCheckQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthCheck>>> = ({
signal,
}) => healthCheck({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof healthCheck>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type HealthCheckQueryResult = NonNullable<
Awaited<ReturnType<typeof healthCheck>>
>;
export type HealthCheckQueryError = ErrorType<unknown>;
/**
* @summary Health check
*/
export function useHealthCheck<
TData = Awaited<ReturnType<typeof healthCheck>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof healthCheck>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getHealthCheckQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* Accepts a request, creates a job row, and issues an eval fee Lightning invoice.
* @summary Create a new agent job
*/
export const getCreateJobUrl = () => {
return `/api/jobs`;
};
export const createJob = async (
createJobRequest: CreateJobRequest,
options?: RequestInit,
): Promise<CreateJobResponse> => {
return customFetch<CreateJobResponse>(getCreateJobUrl(), {
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(createJobRequest),
});
};
export const getCreateJobMutationOptions = <
TError = ErrorType<ErrorResponse>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createJob>>,
TError,
{ data: BodyType<CreateJobRequest> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createJob>>,
TError,
{ data: BodyType<CreateJobRequest> },
TContext
> => {
const mutationKey = ["createJob"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createJob>>,
{ data: BodyType<CreateJobRequest> }
> = (props) => {
const { data } = props ?? {};
return createJob(data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type CreateJobMutationResult = NonNullable<
Awaited<ReturnType<typeof createJob>>
>;
export type CreateJobMutationBody = BodyType<CreateJobRequest>;
export type CreateJobMutationError = ErrorType<ErrorResponse>;
/**
* @summary Create a new agent job
*/
export const useCreateJob = <
TError = ErrorType<ErrorResponse>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createJob>>,
TError,
{ data: BodyType<CreateJobRequest> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationResult<
Awaited<ReturnType<typeof createJob>>,
TError,
{ data: BodyType<CreateJobRequest> },
TContext
> => {
return useMutation(getCreateJobMutationOptions(options));
};
/**
* Returns current job state. Automatically advances the state machine when a pending invoice is found to be paid.
* @summary Get job status
*/
export const getGetJobUrl = (id: string) => {
return `/api/jobs/${id}`;
};
export const getJob = async (
id: string,
options?: RequestInit,
): Promise<JobStatusResponse> => {
return customFetch<JobStatusResponse>(getGetJobUrl(id), {
...options,
method: "GET",
});
};
export const getGetJobQueryKey = (id: string) => {
return [`/api/jobs/${id}`] as const;
};
export const getGetJobQueryOptions = <
TData = Awaited<ReturnType<typeof getJob>>,
TError = ErrorType<ErrorResponse>,
>(
id: string,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetJobQueryKey(id);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getJob>>> = ({
signal,
}) => getJob(id, { signal, ...requestOptions });
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData> & {
queryKey: QueryKey;
};
};
export type GetJobQueryResult = NonNullable<Awaited<ReturnType<typeof getJob>>>;
export type GetJobQueryError = ErrorType<ErrorResponse>;
/**
* @summary Get job status
*/
export function useGetJob<
TData = Awaited<ReturnType<typeof getJob>>,
TError = ErrorType<ErrorResponse>,
>(
id: string,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>;
request?: SecondParameter<typeof customFetch>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetJobQueryOptions(id, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* After a job completes, if the actual cost (tokens used + infra + margin) was
less than the work invoice amount, the difference is owed back to the user.
Submit a BOLT11 invoice for exactly `refundAmountSats` to receive the payment.
Idempotent: returns 409 if already paid or if no refund is owed.
* @summary Claim a refund for overpayment
*/
export const getClaimRefundUrl = (id: string) => {
return `/api/jobs/${id}/refund`;
};
export const claimRefund = async (
id: string,
claimRefundRequest: ClaimRefundRequest,
options?: RequestInit,
): Promise<ClaimRefundResponse> => {
return customFetch<ClaimRefundResponse>(getClaimRefundUrl(id), {
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(claimRefundRequest),
});
};
export const getClaimRefundMutationOptions = <
TError = ErrorType<ErrorResponse>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof claimRefund>>,
TError,
{ id: string; data: BodyType<ClaimRefundRequest> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof claimRefund>>,
TError,
{ id: string; data: BodyType<ClaimRefundRequest> },
TContext
> => {
const mutationKey = ["claimRefund"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof claimRefund>>,
{ id: string; data: BodyType<ClaimRefundRequest> }
> = (props) => {
const { id, data } = props ?? {};
return claimRefund(id, data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type ClaimRefundMutationResult = NonNullable<
Awaited<ReturnType<typeof claimRefund>>
>;
export type ClaimRefundMutationBody = BodyType<ClaimRefundRequest>;
export type ClaimRefundMutationError = ErrorType<ErrorResponse>;
/**
* @summary Claim a refund for overpayment
*/
export const useClaimRefund = <
TError = ErrorType<ErrorResponse>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof claimRefund>>,
TError,
{ id: string; data: BodyType<ClaimRefundRequest> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationResult<
Awaited<ReturnType<typeof claimRefund>>,
TError,
{ id: string; data: BodyType<ClaimRefundRequest> },
TContext
> => {
return useMutation(getClaimRefundMutationOptions(options));
};
/**
* Runs the agent without payment. Limited to 5 requests per IP per hour.
* @summary Free demo (rate-limited)
*/
export const getRunDemoUrl = (params: RunDemoParams) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/demo?${stringifiedParams}`
: `/api/demo`;
};
export const runDemo = async (
params: RunDemoParams,
options?: RequestInit,
): Promise<DemoResponse> => {
return customFetch<DemoResponse>(getRunDemoUrl(params), {
...options,
method: "GET",
});
};
export const getRunDemoQueryKey = (params?: RunDemoParams) => {
return [`/api/demo`, ...(params ? [params] : [])] as const;
};
export const getRunDemoQueryOptions = <
TData = Awaited<ReturnType<typeof runDemo>>,
TError = ErrorType<ErrorResponse>,
>(
params: RunDemoParams,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof runDemo>>, TError, TData>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getRunDemoQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof runDemo>>> = ({
signal,
}) => runDemo(params, { signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof runDemo>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type RunDemoQueryResult = NonNullable<
Awaited<ReturnType<typeof runDemo>>
>;
export type RunDemoQueryError = ErrorType<ErrorResponse>;
/**
* @summary Free demo (rate-limited)
*/
export function useRunDemo<
TData = Awaited<ReturnType<typeof runDemo>>,
TError = ErrorType<ErrorResponse>,
>(
params: RunDemoParams,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof runDemo>>, TError, TData>;
request?: SecondParameter<typeof customFetch>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getRunDemoQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}