build / @flop-lab/x402kit
Pay-per-call kit
Suite entry: flop x402. The README below lists the package options.
Reading progress
FLOP pay-per-call kit
Does my paywalled API speak FLOP's 402 protocol correctly, from both sides? Check a recorded exchange or a local server against quoted SDK rules. Run a mock client and server to inspect the full handshake. The kit also provides request-bound payment handling with a client ceiling and replay store.
Quick start
From the repository root with Node 22 and pnpm. This inspects recorded payment handshake without sending a payment:
pnpm install --frozen-lockfile
pnpm --filter @flop-lab/x402kit build
node packages/x402kit/dist/cli.js check packages/x402kit/fixtures/handshake.jsonPayment boundary
Payment execution is UNVERIFIED. The reference demo fails closed. The shipped SDK does not establish recipient binding for an arbitrary merchant in the HTLC helper. See NOTES.md for the exact code and protocol differences.
Run
Mock handshake and checker
After the quick start, run the server:
node packages/x402kit/dist/cli.js serve-mock 3590In another terminal:
node packages/x402kit/dist/cli.js client http://127.0.0.1:3590/paid
node packages/x402kit/dist/cli.js check http://127.0.0.1:3590/paidThe client prints the captured exchange, counts and checker report. Its token is a deterministic mock value, not a payment. The server releases a labelled mock response. The check command sends one unpaid probe, so its paid-half coverage is incomplete and it exits 3. Use a captured full exchange to check both sides. Redirects are refused. Local requests have a five-second total deadline. Failed probes and proof retries are never repeated automatically. Capture bodies are bounded at 64 KiB. This local diagnostic does not poll shared public services.
flop x402 forwards these same commands after building the umbrella CLI.
The checker accepts the transcript itself or the client output's exchange field.
Headers are arrays of [name, value] pairs, preserving duplicates for diagnosis.
See fixtures/handshake.json for the capture schema. Input is bounded at 128 KiB,
16 rounds and 64 headers per message. Empty and partial captures stay incomplete.
Exit codes: 0 means no deviations among the checked rules, 1 means invalid input or a client failure, 2 means deviations and 3 means incomplete coverage. Findings identify the round, client or server, code, quoted rule and source. A failed paid response stays incomplete unless a supported rule identifies a deviation.
The sdk-0.4.0 profile checks the legacy SDK flow with strict syntax. The
reference profile also requires local request binding and devnet headers.
Stricter local rules are labelled in NOTES.md and src/rules.ts.
The pinned SDK class recording reproduces the repeated unpaid request in
network #89. The refusal response
is scripted. It is not evidence of a live gateway incident or double payment.
Read-only settlement demo
From the repository root with Node 22 and pnpm:
pnpm install
pnpm --filter @flop-lab/x402kit check
pnpm --filter @flop-lab/x402kit build
node packages/x402kit/dist/cli.js --help
node packages/x402kit/dist/cli.js inspect packages/x402kit/fixtures/headers.json
node packages/x402kit/dist/cli.js serve 3590In another terminal:
curl http://127.0.0.1:3590/preview
curl -i -H 'X-Flop-Request-Id: example-request-0001' http://127.0.0.1:3590/settlements/preview returns compact settlement rows with exact escrow arithmetic. It retains
the source and observation time. It caches success and unavailable results for
60 seconds and coalesces concurrent requests. MCP calls use the shared client with
one retry, backoff on 429 and server errors and request spacing. Empty lists remain
empty. /settlements emits a quote but cannot release data for a payment proof.
Prices in quotes are configured examples, not observed network prices.
The template folder runs inside this checkout after installation
and building the package. pnpm dev starts the same Hono reference service.
Importable API
import { checkExchange, runMockClient } from "@flop-lab/x402kit";
const report = checkExchange(recordedExchange);
const local = await runMockClient("http://127.0.0.1:3590/paid");import { paywall, payAndFetch, FileStore } from "@flop-lab/x402kit";
const wall = paywall({
priceFlop: "0.01",
payTo: "UNVERIFIED-NO-RECEIVING-ACCOUNT",
ttl: 60_000,
store: new FileStore("./payments.json"),
});
// No readPayment adapter: every proof fails closed.
const response = await wall(request, () => Response.json({ result: "protected" }));
// The caller must supply an independently audited throwaway-devnet payment adapter.
await payAndFetch(request, {
maxFlop: "0.01",
expectedPayTo: receivingAddress,
authorize: async (quote) => policy.authorize(quote),
pay: auditedDevnetPaymentAdapter,
});honoPaywall(options) wraps the same handler. authorize(quote) must explicitly
return true. It can adapt a spendguard policy without a package dependency. The
quote is frozen. Expiry and cancellation are checked again after authorization.
Redirects are refused. Only one payment callback and one proof retry can occur in
one payAndFetch invocation. A retry failure requires reconciliation using the
existing proof. Calling the helper again without that reconciliation can pay again.
The ceiling covers the quoted FLOP amount. It does not cover chain fees.
The server's readPayment(proof) callback is a trust boundary. It returns:
{ network: "flop-devnet", asset: "FLOP", status: "SETTLED",
finalized: true, canonical: true, hashLock, htlcId, txHash,
amountFlop: "0.01", payTo, blockHash, source, observedAt }Every field must come from independent canonical chain observations. Never construct
this object from client headers. verifyObservedPayment validates the object and
its agreement with the stored quote. It does not read the chain itself.
The quote's random hash-lock field is a reference challenge. This package has no
HTLC preimage lifecycle or redemption path. Adding readPayment alone does not
make a working merchant. The executable flag indicates that the callback contract
was supplied, not that live payment support has been established.
Threat model
The middleware rejects malformed proofs, changed requests, exact-amount mismatches, wrong payees, expired quotes, partial/unfinalized observations and reused transaction hashes. Tests use synthetic payment evidence and recorded real MCP responses.
The request ID is an unguessable bearer capability. Use TLS and keep it out of logs.
A leaked ID and proof can retrieve the cached result. This is not payer authentication.
Transport headers excluded from request binding are listed in bindRequest.
MemoryStore lasts only for the process lifetime. FileStore uses an exclusive
directory lock, flushed writes, atomic rename and directory fsync on local Linux
storage. Multiple workers must use the same store file. A stale lock is never stolen.
Stop all writers and reconcile pending payments before removing one. Network filesystems
and hostile filesystem writers are outside this contract.
Claims are persisted before the handler. Same-process concurrent duplicates share
the result. Competing processes can return 409 payment_pending_reconciliation.
Retry the same proof after the first request completes. Handler errors are cached.
A crash between the claim and response needs manual reconciliation. Exactly-once
external side effects are not promised. There is no automatic refund.
Request bodies are limited to 64 KiB and responses to 1 MiB. Stores admit at most 10,000 quotes and do not evict replay tombstones. Add service-level rate limits and authenticate callers before exposing a paid adapter. This reference does not provide distributed storage, retention compaction or denial-of-service protection.
Checks and evidence
pnpm --filter @flop-lab/x402kit record refreshes the raw SDK evidence, MCP fixture
and local handler flow. Fixtures keep source URLs and UTC timestamps. Unit tests
are offline. FLOP_LIVE=1 enables the optional read-only smoke check.
Failure matrix assertions cover underpayment, overpayment, wrong payee, expiry, replay, changed path/method, partial settlement, reorg, missing finality, concurrent duplicates and malformed headers. These tests do not simulate a real chain payment.
pnpm exec tsx packages/x402kit/test/record-handshake.ts captures a real loopback
exchange on port 3592 plus the pinned SDK's standalone and class flows against a
scripted transport. Fixture dates are capture times, not current quote validity.
The checker compares expiry with the recorded round time. The fuzz corpus exercises
malformed challenge headers before the client callback and malformed proofs before
server release. A real HTTP test covers transport-added headers.
What this does not verify
This kit does not verify real merchant settlement, runtime finality, TEE appraisal, inference correctness or standard x402 v2 interoperability. It has no live payment adapter and creates no wallet. Mock ledger fields and mock success responses are synthetic. The checker does not authenticate captures, recompute the server's request hash, prove exactly-once execution or test every possible SDK extension. Old replay stores need reconciliation of missing HTLC tombstones. The site shows recorded local flows and dated MCP observations. Real payment and confirmation remain UNVERIFIED.