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
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export interface Gateway {
passFrom: (request: Request) => string | null;
/** Whether that token is a live pass this gateway minted. */
verifyPass: (token: string | null) => Promise<boolean>;
/** The x402 offer this gateway would make, without answering a request. */
offer: (days?: number) => Offer;
/** The full 402 body around that offer: the offer plus the pass terms. */
receipt: (days?: number, extra?: Record<string, unknown>) => Offer & Record<string, unknown>;
}

export function createGateway(options: GatewayOptions): Gateway;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/x402-gateway",
"version": "0.5.0",
"version": "0.6.0",
"type": "module",
"description": "Sell crawl access to AI training crawlers by the day over x402, settled by CoinPay. One middleware: 402 with an offer, a sales page with CLI instructions, signed passes, and a robots.txt that keeps search crawlers welcome.",
"keywords": [
Expand Down
14 changes: 14 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,20 @@ export function createGateway(options = {}) {
*/
passFrom,
verifyPass: async (token) => Boolean(token && (await readPass(token, { secret }))),
/**
* The x402 offer this gateway would make, and the full receipt body around
* it, without answering a request.
*
* A site with its own refusal -- a tiered limiter whose 429 body says which
* rung the caller is on and what the next one costs -- wants to keep that
* body and add a machine-readable offer to it, so an x402 client can pay
* on the spot instead of being told in prose to go and fetch /crawl. The
* alternative is every such site rebuilding payTo, price, currency and
* network from its own copy of the config, which is how two of them end up
* quoting different numbers.
*/
offer,
receipt,
/** robots.txt with this gateway's lists and sales path. */
robotsTxt: (extra = {}) =>
robotsTxt({ siteUrl: o.siteUrl, path: o.path, training: o.training, retrieval: o.retrieval, ...extra }),
Expand Down
28 changes: 28 additions & 0 deletions test/quota.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,31 @@ describe("verifyPass", () => {
assert.equal(gate.passFrom(new Request(`${SITE}/`)), null);
});
});

describe('offer and receipt, for a site with its own refusal', () => {
it('hand back what the 402 would have carried, without a request', () => {
const gate = gateway(null);
const offer = gate.offer();
assert.equal(offer.x402Version, 2);
assert.ok(offer.accepts.length > 0);
assert.equal(offer.accepts[0].payTo, PAY_TO);

const receipt = gate.receipt();
assert.equal(receipt.pass.price, '1.00 USD');
assert.equal(receipt.pass.buy, `${SITE}/crawl`);
assert.deepEqual(receipt.accepts, offer.accepts);
});

it('quote more than one day', () => {
const gate = gateway(null);
const one = BigInt(gate.offer(1).accepts[0].amount);
const three = BigInt(gate.offer(3).accepts[0].amount);
assert.equal(three, one * 3n);
assert.equal(gate.receipt(3).pass.days, 3);
});

it('offer nothing when payments are not switched on', () => {
const unpaid = createGateway({ siteUrl: SITE });
assert.deepEqual(unpaid.offer().accepts, []);
});
});
Loading