diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 0000000..2d9982e --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,16 @@ +name: Documentation links +on: + pull_request: + push: + branches: ['**'] +permissions: + contents: read +jobs: + links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/check-internal-links.mjs diff --git a/AUDIT_2026-09-11.md b/AUDIT_2026-09-11.md new file mode 100644 index 0000000..bd87857 --- /dev/null +++ b/AUDIT_2026-09-11.md @@ -0,0 +1,56 @@ +# Documentation audit — 2026-09-11 + +## Canonical source and deployment + +`aifinpay-docs` is the canonical docs source. It is a Mintlify site configured +by `docs.json`, with the intended custom domain `https://docs.aifinpay.io`. +The connected repository and custom-domain state require verification in the +Mintlify project; this checkout contains no provider-side deployment metadata +and has no local Docker deployment step. +`aifp-docs-work/` is a local archive/work folder and is not a Git checkout. + +## Verified findings + +- Main-site developer and docs navigation points to `https://docs.aifinpay.io`. +- The active provider entry point is `https://dash.aifinpay.io`; the + `charge/onboarding` page explicitly sends self-serve providers there. +- The legacy registry bridge flow is retained as an assisted path and does not + advertise `/partners/apply`, `/partners/onboard`, or `/partners/publish-api`. +- `quickstart.mdx` linked to the nonexistent `/mcp` and `/integrations` routes. + Those cards now point to the existing `/pay/mcp` and `/integration` pages, + with copy matching what those pages provide. +- The introduction no longer says settlement is live across 12 networks. It + now points to the readiness reference and describes Polygon as the current + SDK settlement path while per-chain routing rolls out. +- The integration guide now separates stateless receipt signature verification + from stateful quota accounting, documents the one-process limit of the + default `MemoryStore`, and requires a shared atomic store for multi-process + deployments. It also tells manual `/v1/pay` clients to follow any + `payment_authorization` requirement present in their quote. The development + receipt-authorization design was not presented as a production rollout. + +## Validation + +All relative links in the canonical MDX files were checked against the +repository page files, and every page listed by `docs.json` exists. The two +broken links above were the only missing targets found. No payment, API, +contract, or deployment behavior was changed. + +`scripts/check-internal-links.mjs` now provides the same dependency-free check +for local or CI use; it passes with 17 configured pages and no missing links. + +The actual Mintlify local preview found a missing `/favicon.svg` referenced by +`docs.json`. The docs now reuse the main website's existing `favicon.ico`, and +the checker also verifies configured logo and favicon files. The Quickstart +render and corrected next-step links were checked in the local preview. The +Mintlify GitHub check skipped PR11 with "No eligible deployments found"; +this review does not claim that production docs have been updated. + +The Quickstart economics wording was also checked against the canonical pricing +reference and the current SDK settlement validators. The canonical AIFP-1 +route is gross-inclusive, but the Quickstart example targets a provider bridge +whose challenge defines the actual route; it now describes challenge-defined +terms and links to the canonical reference instead of hardcoding a split on a +generic bridge call. The example no longer prints `secret_b58`; it tells users +to keep a reusable secret in a local secret manager or keypair file and out of +shared logs. diff --git a/README.md b/README.md index 8c02cdd..9305f30 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,24 @@ The AiFinPay developer documentation, built with [Mintlify](https://mintlify.com — the same platform our competitors use. Role × language structure: pay for services / charge for your API / reference. -## Go live at docs.aifinpay.io - -1. **Push to GitHub.** Put this folder in a repo (e.g. `AiFinPay/docs`). -2. **Connect Mintlify.** Sign up at https://mintlify.com → **Connect GitHub** → - pick the repo (and this subfolder if it isn't the repo root). Mintlify - rebuilds on every push and gives you a preview at `.mintlify.app`. -3. **Custom domain.** Mintlify dashboard → **Settings → Custom domain** → - `docs.aifinpay.io`. Mintlify shows a CNAME target. -4. **DNS (Cloudflare, `aifinpay.io` zone).** Add a CNAME: - `docs` → the value Mintlify gives (usually `cname.mintlify.app`), per - Mintlify's proxied/DNS-only instruction. -5. **Assets.** Drop logo files at `logo/light.svg` + `logo/dark.svg` and - `favicon.svg` (reuse the ones from `ofh-master/src/assets`). - -Then point the main-site nav **DOCS** link at `https://docs.aifinpay.io` -(currently it points at the interim React `/docs` hub). +## Source and deployment + +This repository is the canonical docs source. The `origin` remote is the +AiFinPay docs repository, whose default branch is `main`; a personal remote is +also configured for mirror or recovery workflows. Work on a branch, review it, +and merge or push through the normal repository workflow. + +The intended deployment mechanism is Mintlify at `https://docs.aifinpay.io`. +The connected repository and custom-domain status must be verified in the +Mintlify project; this checkout contains no provider-side deployment metadata. +Do not create a second application deployment or move the docs into another +repository. Mintlify should read `docs.json` and publish its configured page +tree. The main site already links its **DOCS** navigation to this domain, and +its **DASHBOARD** / provider CTAs go to the self-service customer panel at +[dash.aifinpay.io](https://dash.aifinpay.io). + +The logo and dark/light assets are already checked in under `logo/`; keep them +in place when editing the site. ## Local preview @@ -28,6 +30,14 @@ npm i -g mint mint dev # http://localhost:3000 ``` +## Local link check + +The repository includes a dependency-free check for local page links: + +```bash +node scripts/check-internal-links.mjs +``` + ## Structure ``` diff --git a/docs.json b/docs.json index 66fa3b7..29063c2 100644 --- a/docs.json +++ b/docs.json @@ -8,7 +8,7 @@ "light": "#83AED8", "dark": "#0A0E1A" }, - "favicon": "/favicon.svg", + "favicon": "/favicon.ico", "navigation": { "tabs": [ { diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..142323c Binary files /dev/null and b/favicon.ico differ diff --git a/integration.mdx b/integration.mdx index 95d477c..e840582 100644 --- a/integration.mdx +++ b/integration.mdx @@ -37,7 +37,19 @@ Charging AI agents for your API takes three steps, and one decision. Either way the money path is identical and non-custodial: the agent pays on-chain **from its own wallet to yours** through the settlement contract, and -what your side checks is a signed receipt. Your server never holds funds. +what your side checks is a signed receipt. Signature verification is local and +stateless; quota consumption is stateful and needs a counter store. Your server +never holds funds. + + +The default `MemoryStore` is correct for one process only. If you run PM2, +Node cluster, multiple containers, or more than one pod, pass a shared atomic +store such as `redisStore` to `aifpGate`; otherwise each worker can spend the +same prepaid batch independently. Keep the default `onStoreError: "closed"` +behavior so a counter-store outage returns `503` instead of granting an +uncounted request. The receipt's Ed25519/JWKS signature check does not replace +this stateful metering step. + There is no sandbox yet: the first payment is real money on Polygon mainnet. @@ -60,7 +72,10 @@ The package has two halves with different credentials, on purpose: What the gate does per request: no receipt → answer `HTTP 402` with machine-readable payment instructions; valid receipt → verify the Ed25519 -signature locally, meter the prepaid quota down, let the request through. +signature locally, atomically meter the prepaid quota down in the configured +store, and let the request through. Signature verification needs only the +public JWKS; quota accounting still needs shared state when the gate runs in +more than one process. There is no call to AiFinPay on the hot path — our availability never becomes your latency. @@ -94,7 +109,7 @@ await merchant.ensureResources([ ]); const registry = new ResourceRegistry({ merchant }); -await registry.start(); // fail fast rather than serve with an empty registry +await registry.start(); // load the initial snapshot before serving const app = express(); app.use("/api", aifpGate({ merchantId: merchant.merchantId, registry })); @@ -167,13 +182,16 @@ HTTP 402 "how_to_pay": [ "POST https://api.aifinpay.io/v1/quote {…}", "settle the quoted batch on-chain from your own wallet", - "POST https://api.aifinpay.io/v1/pay {quote_id, chain, asset, tx_ref} -> quota receipt", + "POST https://api.aifinpay.io/v1/pay {quote_id, chain, asset, tx_ref, payment_authorization when the quote requires it} -> quota receipt", "retry this request with header: AIFP-Receipt: " ] } ``` -Agents on the AiFinPay SDK ([Node](/pay/node), [Python](/pay/python), +Keep the complete quote response. When it includes `payment_authorization`, the +client must sign the wallet-bound authorization described by that object and +send it with `/v1/pay`; do not invent or omit that field in a hand-written +client. Agents on the AiFinPay SDK ([Node](/pay/node), [Python](/pay/python), [MCP](/pay/mcp)) resolve that whole conversation from one `agent.pay(url)` call. diff --git a/introduction.mdx b/introduction.mdx index 7f95b3a..ab4d128 100644 --- a/introduction.mdx +++ b/introduction.mdx @@ -1,9 +1,9 @@ --- title: "AiFinPay — Payment Infrastructure for AI Agents" -description: "Stripe for autonomous AI agents. One line of code settles a real on-chain payment over HTTP, then returns the gated response. Non-custodial, multichain, token-free." +description: "Stripe for autonomous AI agents. One line of code settles a real on-chain payment over HTTP on currently enabled routes, then returns the gated response. Non-custodial, multichain, token-free." --- -**AiFinPay is payment infrastructure for autonomous AI agents.** One line of code — `agent.pay(url)` — and your agent settles a real on-chain payment — live across 12 networks — then receives the gated response. +**AiFinPay is payment infrastructure for autonomous AI agents.** One line of code — `agent.pay(url)` — and your agent settles a real on-chain payment on the route advertised by the challenge, then receives the gated response. Current payment-live routes and their evidence are tracked in the [network readiness reference](/reference/networks); the SDK's live settlement path is Polygon while per-chain routing is rolling out. It is the same idea Stripe brought to web checkouts, applied to machine-to-machine commerce: a drop-in rail an agent uses to pay for any API call, and a drop-in way for a provider to charge for one. @@ -12,7 +12,7 @@ It is the same idea Stripe brought to web checkouts, applied to machine-to-machi Agents hold their own keys and sign their own transactions. AiFinPay never takes custody of funds. - Contracts live on 12 mainnet networks — 7 with the full smart-contract stack. SDK settlement is live on Polygon today; per-chain routing is rolling out. + Network inventory and deployment status are tracked by readiness state. SDK settlement is live on Polygon today; per-chain routing is rolling out only as each route passes verification and paid end-to-end checks. No protocol token to buy, hold, or speculate on. Agents pay in MATIC/POL or SOL — the assets they already hold. @@ -89,6 +89,26 @@ both, independently. +## The dashboard + +Everything above can be driven from code alone, but the running product has a +control panel: **[dash.aifinpay.io](https://dash.aifinpay.io)**. + +It is where you register a site, price its endpoints, watch which agents paid +for what, set a payout wallet, and configure webhooks. Whatever you declare in +code with `ensureResources()` shows up there, and whatever you draw there is +picked up by the gate within about a minute — the two surfaces edit the same +registry, so you can start in either one. + + + + Endpoints, pricing tiers, payout wallet, and the paywall builder. + + + Point your domain at AiFinPay, or verify receipts in your own backend. + + + ## SDKs and tooling | Package | Install | Use it for | diff --git a/quickstart.mdx b/quickstart.mdx index 0e707c3..fe4886b 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -41,7 +41,8 @@ hold to use it. agent = Agent.new() print("Fund this address with a few cents of MATIC:", agent.address) - print("Save this secret:", agent.secret_b58) + # If reusing this identity, store agent.secret_b58 in a local secret + # manager or keypair file; never print it into shared logs. ``` ```ts Node @@ -105,9 +106,10 @@ You saw one function call. Under the hood: chain, asset, `payTo` address, amount, and a one-time `nonce`. - The SDK submitted one transaction on Polygon mainnet — a gross-inclusive - AIFP-1 split (merchant 99% / AiFinPay 1% / creator 0%) in one verified tx. No - custodian touches the funds at any point. + The SDK submits one transaction using the settlement terms in the + challenge. It verifies the quoted amount and route economics before + signing; no custodian touches the funds at any point. For canonical AIFP-1 + economics, see [Pricing, units and limits](/reference/pricing-and-limits). The SDK retried your request with the payment proof headers, the @@ -131,12 +133,11 @@ You saw one function call. Under the hood: Full `@aifinpay/agent` API for TypeScript and Node runtimes. - + Zero-code: drop `@aifinpay/mcp` into Claude Desktop, Cursor, or Windsurf and let the model pay for x402-gated APIs itself. - - Paste-and-run examples for LangChain, CrewAI, OpenAI Agents, AutoGPT, - and Flowise. + + Put a payment gate in front of your own API and verify receipts locally. diff --git a/scripts/check-internal-links.mjs b/scripts/check-internal-links.mjs new file mode 100644 index 0000000..d95746f --- /dev/null +++ b/scripts/check-internal-links.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const root = process.cwd(); +const configPath = path.join(root, "docs.json"); +const config = JSON.parse(fs.readFileSync(configPath, "utf8")); +const pages = new Set(); +const files = []; + +function walk(dir) { + for (const name of fs.readdirSync(dir)) { + if (name === ".git" || name === "node_modules") continue; + const file = path.join(dir, name); + const stat = fs.statSync(file); + if (stat.isDirectory()) walk(file); + else if (/\.(md|mdx)$/.test(name)) files.push(file); + } +} + +function configuredPages() { + for (const tab of config.navigation?.tabs ?? []) { + for (const group of tab.groups ?? []) { + for (const page of group.pages ?? []) pages.add(`/${page}`); + } + } +} + +walk(root); +configuredPages(); +const missingFiles = [...pages].filter( + (page) => !fs.existsSync(path.join(root, `${page.slice(1)}.mdx`)), +); +const missingLinks = []; +const missingAssets = [config.favicon, config.logo?.light, config.logo?.dark] + .filter((asset) => typeof asset === "string" && asset.startsWith("/")) + .filter((asset) => !fs.existsSync(path.join(root, asset.slice(1)))); +const linkPattern = /(?:\]\(|href=["'])(\/[A-Za-z0-9_./-]+)/g; + +for (const file of files) { + const source = fs.readFileSync(file, "utf8"); + for (const match of source.matchAll(linkPattern)) { + const target = match[1].replace(/[?#].*$/, ""); + if (!pages.has(target) && !fs.existsSync(path.join(root, `${target.slice(1)}.mdx`))) { + missingLinks.push(`${path.relative(root, file)} -> ${target}`); + } + } +} + +if (missingFiles.length || missingLinks.length || missingAssets.length) { + if (missingFiles.length) console.error("Missing configured pages:\n" + missingFiles.join("\n")); + if (missingLinks.length) console.error("Missing internal links:\n" + missingLinks.join("\n")); + if (missingAssets.length) console.error("Missing configured assets:\n" + missingAssets.join("\n")); + process.exitCode = 1; +} else { + console.log(`OK: ${pages.size} configured pages and no missing internal links`); +}