From f5eb6c0e4522175f7c2b863e7ec67ebc6afe9ea4 Mon Sep 17 00:00:00 2001 From: GianM Date: Sat, 16 May 2026 18:29:45 +0700 Subject: [PATCH 1/5] Release: X402/CDP bazaar deposit + llms.txt docs to production (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add llms.txt intro page for AI agents (#256) Add a GitBook-friendly page introducing PolyPay's llms.txt support so non-technical readers can discover the feature and link AI agents to the live playbook. - New page docs/llms-txt-for-agents.md covering what llms.txt is, the live endpoint, the five integration flows it documents, two ways to wire an agent up, and compatibility notes. - Add the page to docs/SUMMARY.md between x402 and Architecture. - Fix stale api.polypay.xyz references in docs/x402-deposits.md to the real api.polypay.pro host. * feat(x402): add Coinbase CDP bazaar deposit route for agentic.market listing (#260) Adds a second x402 deposit endpoint /x402/bazaar/deposit/:multisig that routes through Coinbase CDP facilitator instead of PayAI, so the resource gets indexed in CDP discovery and surfaces on agentic.market. The default /x402/deposit/:multisig path keeps using PayAI (better rate limits, simpler auth) — no UI change. CDP path activates implicitly when CDP_API_KEY_ID is set; otherwise it returns a clear config error. CDP requires Ed25519 JWT signed per-request (2-min TTL), so the service now lazy-imports @coinbase/x402 createAuthHeader to produce the Authorization header. Payment requirements for the CDP path also embed a declareDiscoveryExtension-shaped extensions.bazaar block (strict JSON Schema), without which CDP would settle but not index. Includes scripts/bazaar-bootstrap.ts to produce the first real settlement that triggers CDP indexing — bootstrap must run against a deployed backend (the resource URL ends up in the catalog). Refs #259 --------- Co-authored-by: BoHsuu <115441679+Huygon764@users.noreply.github.com> From ceb386b27f177d42c73171f93d64e3f69719153b Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Tue, 19 May 2026 22:11:01 +0700 Subject: [PATCH 2/5] fix(x402): index CDP bazaar resource on settle + 402 on unpaid crawl (#272) Two independent reasons the deposit endpoint was never cataloged in the CDP x402 Bazaar, both confirmed against @x402/extensions/bazaar source: - processDeposit threw 400 when X-PAYMENT was missing. The CDP Bazaar crawls the endpoint with the bazaar-extension input (no payment) and only indexes resources that answer 402. Now returns 402 + the PaymentRequired body (canonical x402 unpaid response). - extractDiscoveryInfo reads the bazaar extension from paymentPayload.extensions["bazaar"] for v2; the CDP settle payload sent extensions:{}. Attach the discovery extension (and resource description/mimeType) to the v2 payload so settlement catalogs it. Also add routeTemplate so the multisig-address path segment normalizes to /:multisigAddress, and align schema.required with the SDK (createBodyDiscoveryExtension) so info validates under Ajv 2020. --- packages/backend/src/x402/x402.service.ts | 66 +++++++++++++++++------ 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/x402/x402.service.ts b/packages/backend/src/x402/x402.service.ts index ea195480..8ef3b953 100644 --- a/packages/backend/src/x402/x402.service.ts +++ b/packages/backend/src/x402/x402.service.ts @@ -149,8 +149,20 @@ export class X402Service { facilitator: Facilitator = Facilitator.PayAI, ): Promise { if (facilitator === Facilitator.CDP) this.assertCdpEnabled(); + // No payment yet: respond with 402 + PaymentRequired body (canonical x402). + // The client retries with X-PAYMENT. This is also what the CDP Bazaar + // crawler expects — it POSTs the bazaar-extension input without a payment + // and requires a 402 to index the resource; a 400 here blocks indexing. if (!paymentHeader) { - throw new BadRequestException('Missing X-PAYMENT header'); + const body = await this.buildDiscoveryResponse( + multisigAddress, + resourceUrl, + facilitator, + ); + throw new HttpException( + body as unknown as Record, + HttpStatus.PAYMENT_REQUIRED, + ); } const account = await this.assertAccount(multisigAddress); const payload = decodeXPaymentHeader(paymentHeader); @@ -201,15 +213,19 @@ export class X402Service { resourceUrl, signedAmount, ); + // extractDiscoveryInfo (facilitator.ts) reads the bazaar extension from + // paymentPayload.extensions["bazaar"] for v2 — it is NOT read from the + // 402 response or paymentRequirements. A v1 X-PAYMENT (UI / bootstrap) + // carries no extensions, so we attach the same declaration here. const v2Payload: V2PaymentPayload = { x402Version: 2, - resource: { url: resourceUrl }, + resource: this.buildV2Resource(resourceUrl, account.address), accepted: v2Requirements, payload: { authorization: payload.payload.authorization, signature: payload.payload.signature, }, - extensions: {}, + extensions: { bazaar: this.buildCdpBazaarExtension(resourceUrl) }, }; await this.cdpVerify(v2Payload, v2Requirements); const txHash = await this.cdpSettle(v2Payload, v2Requirements); @@ -492,6 +508,20 @@ export class X402Service { }; } + // extractDiscoveryInfo (SDK @x402/extensions/bazaar) reads resource.description + // and resource.mimeType into the catalog entry, so the same resource object is + // used for both the 402 response and the settle payload to avoid drift. + private buildV2Resource(resourceUrl: string, payTo: string): V2ResourceInfo { + return { + url: resourceUrl, + description: + `Gasless USDC deposit to PolyPay multisig ${payTo}. Sign EIP-3009 ` + + `transferWithAuthorization for any amount in [${MIN_DEPOSIT}, ${MAX_DEPOSIT}] ` + + `(6-decimals USDC).`, + mimeType: 'application/json', + }; + } + private buildV2PaymentRequired( chainId: number, payTo: string, @@ -500,14 +530,7 @@ export class X402Service { ): V2PaymentRequired { return { x402Version: 2, - resource: { - url: resourceUrl, - description: - `Gasless USDC deposit to PolyPay multisig ${payTo}. Sign EIP-3009 ` + - `transferWithAuthorization for any amount in [${MIN_DEPOSIT}, ${MAX_DEPOSIT}] ` + - `(6-decimals USDC).`, - mimeType: 'application/json', - }, + resource: this.buildV2Resource(resourceUrl, payTo), accepts: [ this.buildV2PaymentRequirementsLeaf( chainId, @@ -516,14 +539,24 @@ export class X402Service { amount, ), ], - extensions: { bazaar: this.buildCdpBazaarExtension() }, + extensions: { bazaar: this.buildCdpBazaarExtension(resourceUrl) }, }; } // Mirrors @x402/extensions/bazaar createBodyDiscoveryExtension(...) output - // for a POST endpoint with a JSON body. Shape verified byte-equal against - // the SDK and validateDiscoveryExtension() = true in offline tests. - private buildCdpBazaarExtension(): Record { + // for a POST endpoint with a JSON body. `info` is validated against `schema` + // by the facilitator's validateDiscoveryExtension (Ajv 2020); a mismatch + // makes extractDiscoveryInfo skip the resource. + // + // routeTemplate: the resource path ends in a high-cardinality multisig + // address. extractDiscoveryInfo (facilitator.ts) reads bazaarExtension. + // routeTemplate (sibling of info/schema) and uses it as the canonical + // catalog path, so we replace the address segment with :multisigAddress. + private buildCdpBazaarExtension( + resourceUrl: string, + ): Record { + const path = new URL(resourceUrl).pathname; + const routeTemplate = path.replace(/\/[^/]+$/, '/:multisigAddress'); const inputBodyExample = { memo: 'optional payment memo' }; const inputBodySchema = { type: 'object', @@ -542,6 +575,7 @@ export class X402Service { status: 'SETTLED', }; return { + routeTemplate, info: { input: { type: 'http', @@ -566,7 +600,7 @@ export class X402Service { }, body: inputBodySchema, }, - required: ['type', 'bodyType', 'body'], + required: ['type', 'method', 'bodyType', 'body'], additionalProperties: false, }, output: { From 452698f0dd04122b0cfa7f162cea424d873539f5 Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Thu, 21 May 2026 10:23:10 +0700 Subject: [PATCH 3/5] fix(zkverify): update Horizen testnet zkVerify contract address (#274) --- packages/shared/src/contracts/contracts-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/contracts/contracts-config.ts b/packages/shared/src/contracts/contracts-config.ts index d48fc81c..8213ab1a 100644 --- a/packages/shared/src/contracts/contracts-config.ts +++ b/packages/shared/src/contracts/contracts-config.ts @@ -1,7 +1,7 @@ export const CONTRACT_CONFIG_BY_CHAIN_ID = { 2651420: { // Horizen testnet - zkVerifyAddress: "0xCC02D0A54F3184dF4c88811E5b9FAb7ff8131e4a", + zkVerifyAddress: "0x3098A6974649478f0133046e44105AA84e868C21", vkHash: "0xb3c5381523a496996868370791ec7ae490be7e2c996296fb67708daed8a6ea38", poseidonT3Address: "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93", From 2fbdc1ab005f29a25919ae892785173371241b92 Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Thu, 21 May 2026 12:33:10 +0700 Subject: [PATCH 4/5] chore(backend): horizen testnet wipe script + e2e fixes (#275) * chore(backend): horizen testnet wipe script + e2e fixes after chainId require - Add scripts/wipe-horizen-testnet.ts (yarn wipe:horizen-testnet) to drop all chainId 2651420 accounts + their transactions/votes/signers/contacts /reserved-nonces. Used after the Horizen testnet zkVerify address swap to clear stale state on staging. Safety: refuses when APP_NETWORK=mainnet, unlinks batch_items.contactId before cascading account deletes, --dry-run prints counts without touching data. - Pass chainId in e2e + staging-e2e test utilities for reserveNonce and createTransaction calls. The endpoints started requiring chainId after the (address, chainId) account key rollout; the tests still sent the legacy shape and got 400. - stagingExecuteTransaction: fall back to polling GET /transactions/:txId when the initial POST hangs up (socket reset by edge / proxy idle timeout) instead of bubbling a cryptic ECONNRESET. Server already persists txHash right after on-chain submission so the poll is safe. * chore(docker): include backend scripts/ in runner image for Cloud Run Jobs --- docker/backend.Dockerfile | 3 + packages/backend/package.json | 3 +- .../backend/scripts/wipe-horizen-testnet.ts | 158 ++++++++++++++++++ .../backend/test/e2e/transaction.e2e-spec.ts | 4 + .../test/e2e/transaction.staging.e2e-spec.ts | 4 + .../backend/test/utils/staging-api.util.ts | 66 ++++++-- .../backend/test/utils/transaction.util.ts | 4 +- 7 files changed, 230 insertions(+), 12 deletions(-) create mode 100644 packages/backend/scripts/wipe-horizen-testnet.ts diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile index c058cddb..7a30e705 100644 --- a/docker/backend.Dockerfile +++ b/docker/backend.Dockerfile @@ -91,6 +91,9 @@ COPY --chown=nestjs:nodejs --from=builder /app/packages/backend/prisma.config.ts # Copy assets folder COPY --chown=nestjs:nodejs --from=builder /app/packages/backend/assets ./packages/backend/assets +# Copy one-off maintenance scripts (run via `yarn