usewinch
v0.1.2
Published
Pay-per-crawl middleware — turn any content route into an x402 paywall AI agents can actually pay (USDC on Arc via Circle Gateway). Auto-generates the free /preview and paid /content from your existing pages.
Maintainers
Readme
usewinch
Express middleware that turns any route into an x402-gated nanopayment wall on Arc testnet. Buyers pay fractions of a cent in USDC; settlement is instant via Circle Gateway batching; no payment-processor contract or custodial wallet required for the publisher.
Install
npm install usewinch
# Peer deps (required — the /server bundle hard-imports these at load time):
npm install @x402/core @x402/evm
usewinchships@circle-fin/[email protected]as a direct dependency. No patching is required — 3.2.0 ships with the correctedmaxTimeoutSeconds(604800 s / 7 days) that Circle's facilitator now requires.
Quick start — one line, both routes
createLeptonarcRouter mounts a free /preview/:id (teaser auto-derived from your content) and a paid, 402-gated /content/:id for you — no route boilerplate, no second handler.
import express from "express";
import { createLeptonarcRouter } from "usewinch";
const app = express();
app.use(createLeptonarcRouter({
payTo: "0xYourWallet",
price: "$0.001", // per crawl
publisherKey: "pub_…", // from your leptonarc dashboard
backendUrl: "https://your-leptonarc.app",
content: {
"1": { title: "The Lepton Standard", body: "…full article text…" },
},
}));
app.listen(4030);GET /preview/1→{ id, title, abstract, price, payTo }— free.abstractis auto-excerpted frombody(or pass your own).GET /content/1→ HTTP 402 until the agent pays; then your fullbody, settlement reported to your dashboard.
content may also be an async lookup, so it works with a DB / CMS / filesystem:
createLeptonarcRouter({ payTo, price: "$0.002",
content: async (id) => db.articles.findById(id) }); // → { title, body, abstract? } | nullScaffold a ready-to-run server with:
npx usewinch initNo install-time magic. This package never rewrites your server code on
npm install— auto-injecting middleware via a postinstall script is a supply-chain anti-pattern. You get an explicitapp.use(...)and an opt-innpx usewinch initinstead.
Point it at your live site — fromUrl
Don't want to hand it text at all? Give it your page URLs and it scrapes them for you. Per id (cached): it fetches the page, extracts the real article text (Mozilla Readability — strips nav/ads/boilerplate), and derives the free teaser.
import { createLeptonarcRouter, fromUrl } from "usewinch";
app.use(createLeptonarcRouter({
payTo: "0xYourWallet",
price: "$0.001",
publisherKey: "pub_…",
content: fromUrl((id) => `https://mysite.com/articles/${id}`, {
summarize: "groq", // LLM teaser (optional)
groqApiKey: process.env.GROQ_API_KEY,
// model: "llama-3.1-8b-instant",
// ttlMs: 1000 * 60 * 60, // re-scrape hourly
}),
}));- The paid content is the real scraped page text — it is never LLM-generated.
- The LLM (Groq, OpenAI-compatible) only writes the free abstract, and only from facts in the extracted text. Omit
summarize(or drop the key) and the abstract falls back to plain truncation — the paywall keeps working either way. - Results are cached per id, so the fetch + extract + summarize happens once, not on every crawl.
Lower-level — gate routes you already have
import "dotenv/config";
import express from "express";
import { leptonarcProxy } from "usewinch";
const app = express();
const proxy = leptonarcProxy({
payTo: process.env.SELLER_ADDRESS, // your Arc wallet address
price: "$0.001", // 1000 micro-USDC per request
backendUrl: process.env.LEPTONARC_BACKEND_URL, // optional; logs to console if absent
});
// Free preview — title + abstract + price hint.
app.get("/preview/:id", (req, res) => {
res.json({ price: proxy.config.price, /* ...your metadata */ });
});
// Paid content — the gate + settlement reporter run before your handler.
app.get("/content/:id", ...proxy.content, (req, res) => {
res.json({ body: "full content here" });
});
app.listen(4030);API
leptonarcProxy(config, deps?)
Creates the proxy instance.
| Option | Type | Required | Description |
|---|---|---|---|
| payTo | string | yes | Seller's Arc wallet address (0x…). Settlement goes here. |
| price | string | yes | Price string like "$0.001" (maps to 1000 micro-USDC on 6-dec USDC). |
| publisherKey | string | no | Opaque publisher identifier included in event payloads. |
| backendUrl | string | no | URL of your leptonarc backend (/api/events will be POSTed). Omit to log to console. |
Returns { config, preview, content }:
config— the resolvedProxyConfig(useful for readingpriceback out in preview routes).preview— a single Express handler that returns{ url, price, payTo }— a free teaser.content— a two-handler array[gate, settled]spread directly into a route:app.get("/content/:id", ...proxy.content, yourHandler). The gate returns 402 to unpaid requests;settledfires aPaymentEventto your backend (or console) before calling next.
req.payment (inside paid handlers)
After the gate passes, req.payment is set by the SDK:
{
payer: string; // buyer's wallet address
amount: bigint; // micro-USDC (e.g. 1000n for $0.001)
transaction: string; // settlement UUID (Circle Gateway batch reference)
}Env vars
| Var | Used by | Description |
|---|---|---|
| SELLER_ADDRESS | example/server.ts | The Arc wallet to receive payments. |
| LEPTONARC_BACKEND_URL | leptonarcProxy | Optional backend URL for event reporting. |
| BUYER_PRIVATE_KEY | example/buyer.ts | Buyer's private key for the example buyer script. |
Routes: /preview + /content pattern
GET /preview/:id— free. Returns metadata + price. No 402, no payment needed.GET /content/:id— gated. Returns402 Payment Requiredwith aPAYMENT-REQUIREDbase64 header (x402 v2 protocol) to unpaid callers. A buyer with a fundedGatewayClientpays inline and receives the full response body.
How it settles on Arc
When a buyer calls gateway.pay(url, { method: "GET" }), the GatewayClient (from @circle-fin/x402-batching/client) fetches the 402, signs an EIP-3009 transferWithAuthorization over Circle's Gateway batch wallet, and replays the request with an X-PAYMENT header. The createGatewayMiddleware on the server verifies the signature against Circle's facilitator (https://gateway-api-testnet.circle.com), confirms settlement, and sets req.payment before calling next. Settlement is synchronous from the application's perspective — pay() returns after Circle accepts the signed authorization. On-chain batch settlement lands shortly after.
Full spike findings (chain id, USDC address, Gateway wallet, RPC, timing notes) are documented in docs/superpowers/spikes/2026-06-30-arc-payment-findings.md.
Live settlement proof
The example server (example/server.ts) and buyer (example/buyer.ts) were run against Arc testnet on 2026-07-01 using the spike's funded wallet:
buyer address: 0x1D1B13de8a4024577896Fdf4eE5B47ee3bC6C6eF
settlement UUID: ab138d4d-d241-4993-9a03-c3601f186153
amount paid: 0.001 USDC (1000 micro-USDC)
server log: [leptonarc] paid 1000 micro-USDC for /content/1 by 0x1d1b13de8a4024577896fdf4ee5b47ee3bc6c6ef (ab138d4d-d241-4993-9a03-c3601f186153)No patch was applied to @circle-fin/x402-batching — version 3.2.0 settled cleanly with the default timeout.
Running the example
# From packages/proxy — set .env with SELLER_ADDRESS + BUYER_PRIVATE_KEY
npm run example # starts publisher on :4030
# In a second terminal:
npx tsx example/buyer.ts # deposits 1 USDC, pays /content/1, prints settlement UUID