From 05ddd97b3fdbadc33192648030eb67b1f082990d Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Fri, 15 May 2026 21:44:21 +0300 Subject: [PATCH 1/5] feat(ack-pay): add approval model types --- docs/ack-pay/hitl.mdx | 41 ++++++++++++++++++++++++ packages/ack-pay/src/index.ts | 1 + packages/ack-pay/src/payment-approval.ts | 29 +++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 packages/ack-pay/src/payment-approval.ts diff --git a/docs/ack-pay/hitl.mdx b/docs/ack-pay/hitl.mdx index 2f40e05..dddf0c0 100644 --- a/docs/ack-pay/hitl.mdx +++ b/docs/ack-pay/hitl.mdx @@ -26,4 +26,45 @@ Human oversight may be integrated at three key points in the payment lifecycle: !["Example Human Intervention"](/images/human.png) +## Approval Request and Decision Shape + +ACK-Pay keeps approval orchestration outside the core protocol. Payment Services, +Client applications, and policy systems can use the exported +`PaymentApprovalRequest` and `PaymentApprovalDecision` types as a minimal shared +shape when a payment must pause for a human or owner-controlled policy decision. + +```ts +import type { + PaymentApprovalDecision, + PaymentApprovalRequest, +} from "@agentcommercekit/ack-pay" + +const approvalRequest: PaymentApprovalRequest = { + id: "approval_123", + paymentRequestId: "payment_request_123", + paymentOptionId: "usd-base-usdc", + requesterDid: "did:web:agent.example.com", + reason: "Payment exceeds the autonomous spend limit", + expiresAt: "2026-01-01T12:00:00.000Z", + metadata: { + policyRef: "policy://merchant-spend-v3", + }, +} + +const approvalDecision: PaymentApprovalDecision = { + requestId: approvalRequest.id, + decision: "approved", + approverDid: "did:web:owner.example.com", + decidedAt: new Date().toISOString(), + metadata: { + ticketId: "risk-review-456", + }, +} +``` + +Applications decide how these objects are transported, authenticated, stored, and +bound to execution. A denied decision should stop before signing or settlement; +an approved decision can be recorded in receipt metadata or an application audit +log as supporting evidence. + Integrating these Human-in-the-Loop mechanisms allows organizations to balance the efficiency of automation with the accountability and guardrails provided by human oversight. diff --git a/packages/ack-pay/src/index.ts b/packages/ack-pay/src/index.ts index bd4091f..aecf996 100644 --- a/packages/ack-pay/src/index.ts +++ b/packages/ack-pay/src/index.ts @@ -3,6 +3,7 @@ export * from "./create-payment-request-token" export * from "./errors" export * from "./create-signed-payment-request" export * from "./verify-payment-request-token" +export * from "./payment-approval" export * from "./payment-request" export * from "./receipt-claim-verifier" export * from "./verify-payment-receipt" diff --git a/packages/ack-pay/src/payment-approval.ts b/packages/ack-pay/src/payment-approval.ts new file mode 100644 index 0000000..2cdf071 --- /dev/null +++ b/packages/ack-pay/src/payment-approval.ts @@ -0,0 +1,29 @@ +/** + * A request for human or policy-system approval before a payment is executed. + * + * ACK-Pay does not prescribe an approval workflow. This type gives applications + * and demos a shared object shape for approval-required paths. + */ +export interface PaymentApprovalRequest { + id: string + paymentRequestId: string + paymentOptionId?: string + requesterDid?: string + reason?: string + expiresAt?: string + metadata?: Record +} + +export type PaymentApprovalDecisionValue = "approved" | "denied" + +/** + * The result of a human or policy-system approval request. + */ +export interface PaymentApprovalDecision { + requestId: string + decision: PaymentApprovalDecisionValue + approverDid?: string + reason?: string + decidedAt: string + metadata?: Record +} From 05cd68c664ae42db1c22aa49bf747dd40569f4e6 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 01:09:37 +0300 Subject: [PATCH 2/5] chore: add approval types changeset Signed-off-by: EfeDurmaz16 --- .changeset/add-approval-types.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add-approval-types.md diff --git a/.changeset/add-approval-types.md b/.changeset/add-approval-types.md new file mode 100644 index 0000000..b8da953 --- /dev/null +++ b/.changeset/add-approval-types.md @@ -0,0 +1,5 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Add approval request and decision model types. From 5a358dba462795a2546231f580e32b1b641845ed Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 05:53:12 +0300 Subject: [PATCH 3/5] docs(ack-pay): use future approval expiry placeholder Signed-off-by: EfeDurmaz16 --- docs/ack-pay/hitl.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ack-pay/hitl.mdx b/docs/ack-pay/hitl.mdx index dddf0c0..b027236 100644 --- a/docs/ack-pay/hitl.mdx +++ b/docs/ack-pay/hitl.mdx @@ -45,7 +45,7 @@ const approvalRequest: PaymentApprovalRequest = { paymentOptionId: "usd-base-usdc", requesterDid: "did:web:agent.example.com", reason: "Payment exceeds the autonomous spend limit", - expiresAt: "2026-01-01T12:00:00.000Z", + expiresAt: "", metadata: { policyRef: "policy://merchant-spend-v3", }, From 77fb9d31745eb2284c9dd7b9b2b4434805dd6a32 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 09:54:48 +0300 Subject: [PATCH 4/5] test(ack-pay): cover approval runtime schemas --- packages/ack-pay/src/payment-approval.test.ts | 78 +++++++++++++++++++ packages/ack-pay/src/schemas/valibot.ts | 24 ++++++ packages/ack-pay/src/schemas/zod/v3.ts | 24 ++++++ packages/ack-pay/src/schemas/zod/v4.ts | 24 ++++++ 4 files changed, 150 insertions(+) create mode 100644 packages/ack-pay/src/payment-approval.test.ts diff --git a/packages/ack-pay/src/payment-approval.test.ts b/packages/ack-pay/src/payment-approval.test.ts new file mode 100644 index 0000000..6b8dedf --- /dev/null +++ b/packages/ack-pay/src/payment-approval.test.ts @@ -0,0 +1,78 @@ +import * as v from "valibot" +import { describe, expect, it } from "vitest" + +import * as valibot from "./schemas/valibot" +import * as zodv3 from "./schemas/zod/v3" +import * as zodv4 from "./schemas/zod/v4" + +const schemas = { + valibot: { + approvalRequest: (value: unknown) => + v.safeParse(valibot.paymentApprovalRequestSchema, value).success, + approvalDecision: (value: unknown) => + v.safeParse(valibot.paymentApprovalDecisionSchema, value).success, + }, + zodv3: { + approvalRequest: (value: unknown) => + zodv3.paymentApprovalRequestSchema.safeParse(value).success, + approvalDecision: (value: unknown) => + zodv3.paymentApprovalDecisionSchema.safeParse(value).success, + }, + zodv4: { + approvalRequest: (value: unknown) => + zodv4.paymentApprovalRequestSchema.safeParse(value).success, + approvalDecision: (value: unknown) => + zodv4.paymentApprovalDecisionSchema.safeParse(value).success, + }, +} + +describe.each(Object.entries(schemas))( + "payment approval schemas (%s)", + (_name, schema) => { + it("validates approval requests", () => { + expect( + schema.approvalRequest({ + id: "approval-1", + paymentRequestId: "payment-request-1", + paymentOptionId: "usdc-base", + requesterDid: "did:web:merchant.example", + reason: "Amount exceeds automated policy limit.", + expiresAt: "2026-01-01T00:00:00.000Z", + metadata: { policyId: "spend-limit" }, + }), + ).toBe(true) + }) + + it("rejects malformed approval requests", () => { + expect( + schema.approvalRequest({ + id: "approval-1", + requesterDid: "not-a-did", + }), + ).toBe(false) + }) + + it("validates approval decisions", () => { + expect( + schema.approvalDecision({ + requestId: "approval-1", + decision: "approved", + approverDid: "did:web:operator.example", + reason: "Reviewed by operator.", + decidedAt: "2026-01-01T00:01:00.000Z", + metadata: { ticketId: "ticket-1" }, + }), + ).toBe(true) + }) + + it("rejects malformed approval decisions", () => { + expect( + schema.approvalDecision({ + requestId: "approval-1", + decision: "pending", + decidedAt: "2026-01-01T00:01:00.000Z", + }), + ).toBe(false) + }) + }, +) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 06a58f1..f8cfb88 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -36,3 +36,27 @@ export const paymentReceiptClaimSchema = v.object({ paymentOptionId: v.string(), metadata: v.optional(v.record(v.string(), v.unknown())), }) + +export const paymentApprovalRequestSchema = v.object({ + id: v.string(), + paymentRequestId: v.string(), + paymentOptionId: v.optional(v.string()), + requesterDid: v.optional(didUriSchema), + reason: v.optional(v.string()), + expiresAt: v.optional(v.string()), + metadata: v.optional(v.record(v.string(), v.unknown())), +}) + +export const paymentApprovalDecisionValueSchema = v.union([ + v.literal("approved"), + v.literal("denied"), +]) + +export const paymentApprovalDecisionSchema = v.object({ + requestId: v.string(), + decision: paymentApprovalDecisionValueSchema, + approverDid: v.optional(didUriSchema), + reason: v.optional(v.string()), + decidedAt: v.string(), + metadata: v.optional(v.record(v.string(), v.unknown())), +}) diff --git a/packages/ack-pay/src/schemas/zod/v3.ts b/packages/ack-pay/src/schemas/zod/v3.ts index 2e3ee83..0934da5 100644 --- a/packages/ack-pay/src/schemas/zod/v3.ts +++ b/packages/ack-pay/src/schemas/zod/v3.ts @@ -31,3 +31,27 @@ export const paymentReceiptClaimSchema = z.object({ paymentOptionId: z.string(), metadata: z.record(z.string(), z.unknown()).optional(), }) + +export const paymentApprovalRequestSchema = z.object({ + id: z.string(), + paymentRequestId: z.string(), + paymentOptionId: z.string().optional(), + requesterDid: didUriSchema.optional(), + reason: z.string().optional(), + expiresAt: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}) + +export const paymentApprovalDecisionValueSchema = z.union([ + z.literal("approved"), + z.literal("denied"), +]) + +export const paymentApprovalDecisionSchema = z.object({ + requestId: z.string(), + decision: paymentApprovalDecisionValueSchema, + approverDid: didUriSchema.optional(), + reason: z.string().optional(), + decidedAt: z.string(), + metadata: z.record(z.string(), z.unknown()).optional(), +}) diff --git a/packages/ack-pay/src/schemas/zod/v4.ts b/packages/ack-pay/src/schemas/zod/v4.ts index 55c19f0..62cf2d8 100644 --- a/packages/ack-pay/src/schemas/zod/v4.ts +++ b/packages/ack-pay/src/schemas/zod/v4.ts @@ -31,3 +31,27 @@ export const paymentReceiptClaimSchema = z.object({ paymentOptionId: z.string(), metadata: z.record(z.string(), z.unknown()).optional(), }) + +export const paymentApprovalRequestSchema = z.object({ + id: z.string(), + paymentRequestId: z.string(), + paymentOptionId: z.string().optional(), + requesterDid: didUriSchema.optional(), + reason: z.string().optional(), + expiresAt: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}) + +export const paymentApprovalDecisionValueSchema = z.union([ + z.literal("approved"), + z.literal("denied"), +]) + +export const paymentApprovalDecisionSchema = z.object({ + requestId: z.string(), + decision: paymentApprovalDecisionValueSchema, + approverDid: didUriSchema.optional(), + reason: z.string().optional(), + decidedAt: z.string(), + metadata: z.record(z.string(), z.unknown()).optional(), +}) From 44e73f03e2e977eb0fc583128117e6774ad739bc Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 10:13:15 +0300 Subject: [PATCH 5/5] fix(ack-pay): normalize approval timestamps --- packages/ack-pay/src/payment-approval.test.ts | 20 +++++++++++++++++++ packages/ack-pay/src/schemas/valibot.ts | 16 +++++++-------- packages/ack-pay/src/schemas/zod/v3.ts | 13 ++++++------ packages/ack-pay/src/schemas/zod/v4.ts | 13 ++++++------ 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/ack-pay/src/payment-approval.test.ts b/packages/ack-pay/src/payment-approval.test.ts index 6b8dedf..b5ba93c 100644 --- a/packages/ack-pay/src/payment-approval.test.ts +++ b/packages/ack-pay/src/payment-approval.test.ts @@ -43,6 +43,16 @@ describe.each(Object.entries(schemas))( ).toBe(true) }) + it("accepts Date timestamps in approval requests", () => { + expect( + schema.approvalRequest({ + id: "approval-1", + paymentRequestId: "payment-request-1", + expiresAt: new Date("2026-01-01T00:00:00.000Z"), + }), + ).toBe(true) + }) + it("rejects malformed approval requests", () => { expect( schema.approvalRequest({ @@ -65,6 +75,16 @@ describe.each(Object.entries(schemas))( ).toBe(true) }) + it("accepts Date timestamps in approval decisions", () => { + expect( + schema.approvalDecision({ + requestId: "approval-1", + decision: "approved", + decidedAt: new Date("2026-01-01T00:01:00.000Z"), + }), + ).toBe(true) + }) + it("rejects malformed approval decisions", () => { expect( schema.approvalDecision({ diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index f8cfb88..1554e89 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -4,6 +4,11 @@ import * as v from "valibot" const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) +const timestampSchema = v.pipe( + v.union([v.date(), v.string()]), + v.transform((input) => new Date(input).toISOString()), +) + export const paymentOptionSchema = v.object({ id: v.string(), amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]), @@ -19,12 +24,7 @@ export const paymentRequestSchema = v.object({ id: v.string(), description: v.optional(v.string()), serviceCallback: v.optional(v.pipe(v.string(), v.url())), - expiresAt: v.optional( - v.pipe( - v.union([v.date(), v.string()]), - v.transform((input) => new Date(input).toISOString()), - ), - ), + expiresAt: v.optional(timestampSchema), paymentOptions: v.pipe( v.tupleWithRest([paymentOptionSchema], paymentOptionSchema), v.nonEmpty(), @@ -43,7 +43,7 @@ export const paymentApprovalRequestSchema = v.object({ paymentOptionId: v.optional(v.string()), requesterDid: v.optional(didUriSchema), reason: v.optional(v.string()), - expiresAt: v.optional(v.string()), + expiresAt: v.optional(timestampSchema), metadata: v.optional(v.record(v.string(), v.unknown())), }) @@ -57,6 +57,6 @@ export const paymentApprovalDecisionSchema = v.object({ decision: paymentApprovalDecisionValueSchema, approverDid: v.optional(didUriSchema), reason: v.optional(v.string()), - decidedAt: v.string(), + decidedAt: timestampSchema, metadata: v.optional(v.record(v.string(), v.unknown())), }) diff --git a/packages/ack-pay/src/schemas/zod/v3.ts b/packages/ack-pay/src/schemas/zod/v3.ts index 0934da5..95178a1 100644 --- a/packages/ack-pay/src/schemas/zod/v3.ts +++ b/packages/ack-pay/src/schemas/zod/v3.ts @@ -4,6 +4,10 @@ import { z } from "zod/v3" const urlOrDidUri = z.union([z.string().url(), didUriSchema]) +const timestampSchema = z + .union([z.date(), z.string()]) + .transform((val) => new Date(val).toISOString()) + export const paymentOptionSchema = z.object({ id: z.string(), amount: z.union([z.number().int().positive(), z.string()]), @@ -19,10 +23,7 @@ export const paymentRequestSchema = z.object({ id: z.string(), description: z.string().optional(), serviceCallback: z.string().url().optional(), - expiresAt: z - .union([z.date(), z.string()]) - .transform((val) => new Date(val).toISOString()) - .optional(), + expiresAt: timestampSchema.optional(), paymentOptions: z.array(paymentOptionSchema).nonempty(), }) @@ -38,7 +39,7 @@ export const paymentApprovalRequestSchema = z.object({ paymentOptionId: z.string().optional(), requesterDid: didUriSchema.optional(), reason: z.string().optional(), - expiresAt: z.string().optional(), + expiresAt: timestampSchema.optional(), metadata: z.record(z.string(), z.unknown()).optional(), }) @@ -52,6 +53,6 @@ export const paymentApprovalDecisionSchema = z.object({ decision: paymentApprovalDecisionValueSchema, approverDid: didUriSchema.optional(), reason: z.string().optional(), - decidedAt: z.string(), + decidedAt: timestampSchema, metadata: z.record(z.string(), z.unknown()).optional(), }) diff --git a/packages/ack-pay/src/schemas/zod/v4.ts b/packages/ack-pay/src/schemas/zod/v4.ts index 62cf2d8..8223d53 100644 --- a/packages/ack-pay/src/schemas/zod/v4.ts +++ b/packages/ack-pay/src/schemas/zod/v4.ts @@ -4,6 +4,10 @@ import * as z from "zod/v4" const urlOrDidUri = z.union([z.url(), didUriSchema]) +const timestampSchema = z + .union([z.date(), z.string()]) + .transform((val) => new Date(val).toISOString()) + export const paymentOptionSchema = z.object({ id: z.string(), amount: z.union([z.number().int().positive(), z.string()]), @@ -19,10 +23,7 @@ export const paymentRequestSchema = z.object({ id: z.string(), description: z.string().optional(), serviceCallback: z.url().optional(), - expiresAt: z - .union([z.date(), z.string()]) - .transform((val) => new Date(val).toISOString()) - .optional(), + expiresAt: timestampSchema.optional(), paymentOptions: z.array(paymentOptionSchema).nonempty(), }) @@ -38,7 +39,7 @@ export const paymentApprovalRequestSchema = z.object({ paymentOptionId: z.string().optional(), requesterDid: didUriSchema.optional(), reason: z.string().optional(), - expiresAt: z.string().optional(), + expiresAt: timestampSchema.optional(), metadata: z.record(z.string(), z.unknown()).optional(), }) @@ -52,6 +53,6 @@ export const paymentApprovalDecisionSchema = z.object({ decision: paymentApprovalDecisionValueSchema, approverDid: didUriSchema.optional(), reason: z.string().optional(), - decidedAt: z.string(), + decidedAt: timestampSchema, metadata: z.record(z.string(), z.unknown()).optional(), })