Add honest accounting and automatic refund mechanism for completed jobs
Implement honest accounting post-job completion, calculating actual costs, adding margin, and enabling automatic refunds for overpayments via a new endpoint. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: c6386de2-d5f4-47cc-a557-73416f09e118 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:
@@ -53,7 +53,19 @@ export interface PricingBreakdown {
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual cost record stored after the job completes
|
||||
* 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;
|
||||
@@ -62,8 +74,20 @@ export interface CostLedger {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -79,6 +103,18 @@ export interface JobStatusResponse {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import type {
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
ClaimRefundRequest,
|
||||
ClaimRefundResponse,
|
||||
CreateJobRequest,
|
||||
CreateJobResponse,
|
||||
DemoResponse,
|
||||
@@ -274,6 +276,98 @@ export function useGetJob<
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user