Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/DEPLOY_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,7 @@ Arc 非対応・自前の汎用 EIP-3009 facilitator は作らない)。設計 =
ローカル `next start` (`X402_NETWORK=base-sepolia`・`ENABLE_X402_ARC_GATEWAY=1`) の `/api/paid/hello` を
Arc accept で購入 → 200 + PAYMENT-RESPONSE success → 売り手の Gateway 残高 (domain 26) 増加を確認。
2. Vercel Production に `ENABLE_X402_ARC_GATEWAY=1` → 開示 3 点セット同期 PR (掟 14・LP FAQ / Terms / llms.txt /
README / お知らせ・**`/openapi.json` (`lib/openapi/document.ts` の `network`/`x-payment-chains`)**) を同一リリースで
README / お知らせ・**`/openapi.json` (`lib/openapi/payment.ts` の `network`/`x-payment-chains`)**) を同一リリースで
merge → deploy。
3. 本番 smoke: `node scripts/arc-gateway-buyer-smoke.mjs` → `http://localhost:4599` をウォレットのあるブラウザで開き、
接続 → (初回のみ) USDC を Gateway Wallet に deposit → `/api/paid/hello` を購入 (秘密鍵を扱わない・署名はウォレット内・
Expand Down
89 changes: 89 additions & 0 deletions lib/openapi/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// JPYC ネットワーク活動 (24h 集計)・送金証明 (attest)・無料 preview の operation。

import {
ACTIVITY_CHAINS,
USDC_JPYC_ACTIVITY,
USDC_JPYC_ATTEST,
agentUsageText,
} from '@/lib/jpyc/liveResources';
import { JPYC_ACTIVITY_PREVIEW_SCHEMA } from '@/lib/jpyc/liveSchema';
import { usdcPaymentChains, usdcPaymentInfo } from '@/lib/openapi/payment';
import { JPYC_LIVE_402 } from '@/lib/openapi/schema';

const JPYC_ACTIVITY_400 = {
description: 'Unknown or duplicate query key, unsupported chain, empty/invalid window, or missing chain with payment. No settlement.',
};
export const ACTIVITY_OPENAPI_PATHS = {
[USDC_JPYC_ACTIVITY.path]: {
get: {
tags: ['x402 Vanilla (USDC)', 'JPYC Live Data'],
operationId: USDC_JPYC_ACTIVITY.operationId,
summary: USDC_JPYC_ACTIVITY.summary,
description: USDC_JPYC_ACTIVITY.description + ' ' + agentUsageText(USDC_JPYC_ACTIVITY.trigger) + ' Payment: standard x402 in USDC on Base mainnet; no OpenPay fee is added.',
parameters: [
{ name: 'chain', in: 'query', required: true, schema: { type: 'string', enum: ACTIVITY_CHAINS } },
{ name: 'window', in: 'query', required: false, schema: { type: 'string', enum: ['24h'], default: '24h' } },
],
'x-agent-usage': USDC_JPYC_ACTIVITY.trigger,
'x-payment-info': usdcPaymentInfo(USDC_JPYC_ACTIVITY.priceUsd),
'x-payment-protocol': 'x402', 'x-payment-asset': 'USDC', 'x-payment-chains': usdcPaymentChains(),
responses: {
'200': {
description: 'Complete aggregate from immutable finalized buckets after settlement; observedAt is the newest bucket timestamp and expiresAt is four hours later.',
content: { 'application/json': {
schema: USDC_JPYC_ACTIVITY.bazaar.output.schema, example: USDC_JPYC_ACTIVITY.bazaar.output.example,
} },
},
'400': JPYC_ACTIVITY_400,
'402': JPYC_LIVE_402,
'503': {
description: 'data_incomplete: a required bucket is missing. data_unavailable: KV is unavailable, malformed or overflowed, or the data timestamp is over 60 seconds in the future. data_stale: the newest bucket timestamp is more than four hours old. No settlement in every case.',
},
},
},
},
[USDC_JPYC_ATTEST.path]: {
get: {
tags: ['x402 Vanilla (USDC)', 'JPYC Live Data'],
operationId: USDC_JPYC_ATTEST.operationId,
summary: USDC_JPYC_ATTEST.summary,
description: USDC_JPYC_ATTEST.description + ' ' + agentUsageText(USDC_JPYC_ATTEST.trigger) + ' Payment: standard x402 in USDC on Base mainnet; no OpenPay fee is added.',
parameters: [
{ name: 'chain', in: 'query', required: true, schema: USDC_JPYC_ATTEST.bazaar.queryParamsSchema.properties.chain },
{ name: 'tx', in: 'query', required: true, schema: USDC_JPYC_ATTEST.bazaar.queryParamsSchema.properties.tx },
],
'x-agent-usage': USDC_JPYC_ATTEST.trigger,
'x-payment-info': usdcPaymentInfo(USDC_JPYC_ATTEST.priceUsd),
'x-payment-protocol': 'x402', 'x-payment-asset': 'USDC', 'x-payment-chains': usdcPaymentChains(),
responses: {
'200': {
description: 'JPYC transfers with an optional EIP-712 signature. The signature is not a legal certification.',
content: { 'application/json': {
schema: USDC_JPYC_ATTEST.bazaar.output.schema, example: USDC_JPYC_ATTEST.bazaar.output.example,
} },
},
'400': { description: 'Invalid or duplicate query parameters; missing required parameters with payment. No settlement.' },
'404': { description: 'tx_not_found: receipt not mined or absent. no_jpyc_transfer: reverted or no JPYC Transfer logs. No settlement.' },
'402': JPYC_LIVE_402,
'503': {
description: 'RPC unavailable. No settlement.',
},
},
},
},
'/api/jpyc/activity/preview': {
get: {
tags: ['JPYC Live Data'], operationId: 'getJpycNetworkActivityPreview',
summary: 'Preview JPYC network activity availability and transfer count',
description: 'Free preview using the same finalized buckets and validity checks as the paid feed. Skip a purchase when observedAt is unchanged or expiresAt has passed. Unavailable data has a reason and no sample. No feature flag is required.',
parameters: [{ name: 'chain', in: 'query', required: false, schema: { type: 'string', enum: ACTIVITY_CHAINS, default: 'polygon' } }],
responses: {
'200': {
description: 'Stable available/unavailable envelope. Available cache freshness plus stale-while-revalidate is capped by expiresAt; unavailable responses cache for 60 seconds.',
content: { 'application/json': { schema: JPYC_ACTIVITY_PREVIEW_SCHEMA } },
},
'400': { description: 'Unknown or duplicate query key, or unsupported/empty chain.' },
},
},
},
} as const;
281 changes: 281 additions & 0 deletions lib/openapi/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// 基底文書 (lib/openapi/document.ts の OPENAPI_DOCUMENT) の components。schemas は Directory の
// 型と全領域共有の Error、responses は全領域が $ref する共通応答 (StorageUnavailable は
// buildOpenApiDocument が生成時に足す)。

export const BASE_OPENAPI_SCHEMAS = {
DirectoryEntry: {
type: 'object',
required: [
'slug',
'name',
'nameJa',
'status',
'sourceUrl',
'sourceType',
'verifiedAt',
'updatedAt',
'attribution',
'facts',
'editorial',
'sourceCheckedAt',
'sourceOk',
],
properties: {
slug: { type: 'string' },
name: { type: 'string' },
nameJa: { type: 'string' },
status: { type: 'string', const: 'published' },
sourceUrl: { type: 'string', format: 'uri' },
sourceType: { type: 'string', enum: ['official', 'manual'] },
verifiedAt: { type: 'string', format: 'date' },
updatedAt: { type: 'string', format: 'date' },
sourceCheckedAt: { type: ['string', 'null'], format: 'date-time' },
sourceOk: {
type: ['boolean', 'null'],
description:
'Source URL reachability only; it does not establish whether the directory information is true. true = reachable (2xx/3xx), false = confirmed gone (404/410), null = indeterminate (no current result, bot protection, or transient failure).',
},
attribution: { type: 'string' },
facts: {
type: 'object',
required: [
'description',
'category',
'tags',
'tokens',
'chains',
'languages',
'supportsJpyc',
'supportsUsdc',
'supportsX402',
'supportsMcp',
],
properties: {
description: { type: 'string' },
category: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
tokens: { type: 'array', items: { type: 'string' } },
chains: { type: 'array', items: { type: 'string' } },
languages: { type: 'array', items: { type: 'string' } },
supportsJpyc: { type: 'boolean' },
supportsUsdc: { type: 'boolean' },
supportsX402: { type: 'boolean' },
supportsMcp: { type: 'boolean' },
},
},
editorial: {
type: 'object',
required: ['summaryJa', 'summaryEn'],
properties: {
summaryJa: { type: 'string' },
summaryEn: { type: 'string' },
},
},
},
},
DirectoryLicensedEnvelope: {
type: 'object',
allOf: [
{ $ref: '#/components/schemas/DirectoryEnvelope' },
{
type: 'object',
required: ['license', 'attestation', 'signer', 'verify'],
properties: {
license: {
type: 'object',
required: ['id', 'name', 'url', 'licensee', 'issuedAt', 'grants', 'requires', 'prohibits'],
properties: {
id: { type: 'string', const: 'openpay-directory-license-v1' },
name: { type: 'string', const: 'OpenPay Directory Data License v1' },
url: { type: 'string', format: 'uri' },
licensee: { type: ['string', 'null'], pattern: '^0x[0-9a-fA-F]{40}$' },
issuedAt: { type: 'string', format: 'date-time' },
grants: { type: 'array', items: { type: 'string' } },
requires: { type: 'array', items: { type: 'string' } },
prohibits: { type: 'array', items: { type: 'string' } },
},
},
attestation: {
type: ['object', 'null'],
required: ['message', 'signature'],
properties: {
message: {
type: 'object',
required: ['licensee', 'licenseId', 'contentHash', 'rows', 'issuedAt'],
properties: {
licensee: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$', description: 'Zero address when the payer is unknown.' },
licenseId: { type: 'string', const: 'openpay-directory-license-v1' },
contentHash: { type: 'string', pattern: '^0x[0-9a-fA-F]{64}$', description: 'keccak256 of UTF-8 JSON.stringify(items), preserving array order.' },
rows: { type: 'integer', minimum: 0 },
issuedAt: { type: 'integer', minimum: 0, description: 'Unix seconds.' },
},
},
signature: { type: 'string', pattern: '^0x[0-9a-fA-F]{130}$' },
},
},
signer: { type: ['string', 'null'], pattern: '^0x[0-9a-fA-F]{40}$' },
verify: {
type: 'object',
required: ['method', 'domain', 'types'],
properties: {
method: { type: 'string', const: 'EIP-712 recoverTypedDataAddress' },
domain: {
type: 'object',
required: ['name', 'version'],
additionalProperties: false,
properties: {
name: { type: 'string', const: 'OpenPay Directory License' },
version: { type: 'string', const: '1' },
},
},
types: {
type: 'object',
required: ['DirectoryLicense'],
additionalProperties: false,
properties: {
DirectoryLicense: {
type: 'array',
const: [
{ name: 'licensee', type: 'address' },
{ name: 'licenseId', type: 'string' },
{ name: 'contentHash', type: 'bytes32' },
{ name: 'rows', type: 'uint256' },
{ name: 'issuedAt', type: 'uint256' },
],
items: {
type: 'object',
required: ['name', 'type'],
properties: { name: { type: 'string' }, type: { type: 'string' } },
},
},
},
},
},
},
},
},
],
},
DirectoryEnvelope: {
type: 'object',
required: [
'schemaVersion',
'query',
'items',
'total',
'generatedAt',
'dataFreshness',
'licenseNotice',
'attribution',
],
properties: {
schemaVersion: { type: 'string', const: '1.0' },
query: { type: 'object' },
items: {
type: 'array',
items: { $ref: '#/components/schemas/DirectoryEntry' },
},
total: { type: 'integer', minimum: 0 },
generatedAt: { type: 'string', format: 'date-time' },
dataFreshness: {
type: 'object',
required: ['oldest', 'newestVerifiedAt', 'oldestSourceCheckedAt'],
properties: {
oldest: { type: ['string', 'null'], format: 'date' },
newestVerifiedAt: { type: ['string', 'null'], format: 'date' },
oldestSourceCheckedAt: {
type: ['string', 'null'],
format: 'date-time',
},
},
},
licenseNotice: {
type: 'string',
description:
'sourceOk reports source URL reachability only, not whether the information is true.',
},
attribution: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
},
},
Error: {
type: 'object',
required: ['ok', 'error'],
properties: {
ok: { type: 'boolean', const: false },
error: {
type: 'string',
enum: [
'invalid_query',
'not_found',
'rate_limited',
'storage_unavailable',
],
},
},
},
} as const;

export const BASE_OPENAPI_RESPONSES = {
InvalidQuery: {
description: 'A query value is outside the documented allowlist.',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
example: { ok: false, error: 'invalid_query' },
},
},
},
NotFound: {
description: 'Feature disabled, slug absent, or entry not published.',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
example: { ok: false, error: 'not_found' },
},
},
},
RateLimited: {
description: 'Best-effort per-IP request limit exceeded.',
headers: {
'Retry-After': { schema: { type: 'integer' }, description: 'Seconds' },
},
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
example: { ok: false, error: 'rate_limited' },
},
},
},
PaymentRequired: {
description:
'x402 payment challenge. Amount is denominated in JPYC on Polygon or Polygon Amoy; the existing buyer-added facilitator fee is included in maxAmountRequired.',
headers: {
'PAYMENT-REQUIRED': {
schema: { type: 'string' },
description: 'Base64-encoded x402 v2 payment requirements.',
},
},
content: {
'application/json': {
example: {
x402Version: 1,
accepts: [
{
scheme: 'exact',
network: 'eip155:137',
resource:
'https://open-pay.jp/api/paid/japan-web3-directory',
maxAmountRequired: '3000000000000000000',
asset: 'JPYC',
},
],
error: 'payment_required',
},
},
},
},
} as const;
Loading
Loading