@oathbuild/x402 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/index.d.ts +168 -0
- package/dist/index.js +217 -0
- package/package.json +32 -3
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @oathbuild/x402
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OATH is a Transaction Builder for autonomous agents. It builds complete agent transaction flows across MCP, x402 and onchain actions, with authorization built into the execution path.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @oathbuild/x402 viem
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createX402 } from "@oathbuild/x402"
|
|
11
|
+
|
|
12
|
+
const x402 = createX402({ oath, payer: agent })
|
|
13
|
+
const res = await x402.fetch(url) // answers a 402 by paying, if the OATH allows it
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Supports the exact scheme over EIP-3009 and Permit2, and x402 over MCP. A payment outside the service's cap or the OATH's limit is refused before anything is signed. OATH integrates x402 as it exists; it does not define its own payment standard.
|
|
17
|
+
|
|
18
|
+
## Links
|
|
19
|
+
|
|
20
|
+
- Docs: https://oathbuild.net/docs
|
|
21
|
+
- Contract addresses: https://oathbuild.net/docs#/deployments
|
|
22
|
+
- App: https://oathbuild.net/app
|
|
23
|
+
|
|
24
|
+
MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
import { type Address, type Hex, type LocalAccount } from 'viem';
|
|
3
|
+
import type { Oath, ActionInput } from '@oathbuild/core';
|
|
4
|
+
export type PaymentRequirements = {
|
|
5
|
+
scheme: string;
|
|
6
|
+
network: string;
|
|
7
|
+
amount: string;
|
|
8
|
+
asset: string;
|
|
9
|
+
payTo: string;
|
|
10
|
+
maxTimeoutSeconds: number;
|
|
11
|
+
extra?: {
|
|
12
|
+
name?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
assetTransferMethod?: string;
|
|
15
|
+
[k: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type PaymentRequired = {
|
|
19
|
+
x402Version: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
resource?: {
|
|
22
|
+
url: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
};
|
|
26
|
+
accepts: PaymentRequirements[];
|
|
27
|
+
};
|
|
28
|
+
export type PaymentPayload = {
|
|
29
|
+
x402Version: 2;
|
|
30
|
+
resource?: PaymentRequired['resource'];
|
|
31
|
+
accepted: PaymentRequirements;
|
|
32
|
+
payload: unknown;
|
|
33
|
+
};
|
|
34
|
+
export type Payment = {
|
|
35
|
+
serviceId: string;
|
|
36
|
+
url: string;
|
|
37
|
+
scheme: string;
|
|
38
|
+
network: string;
|
|
39
|
+
asset: string;
|
|
40
|
+
amount: bigint;
|
|
41
|
+
payTo: string;
|
|
42
|
+
settlement: unknown;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
};
|
|
45
|
+
export declare class X402Error extends Error {
|
|
46
|
+
code: string;
|
|
47
|
+
constructor(code: string, message: string);
|
|
48
|
+
}
|
|
49
|
+
export declare const decodePaymentRequired: (header: string) => PaymentRequired;
|
|
50
|
+
export type SchemeContext = {
|
|
51
|
+
oath: Oath;
|
|
52
|
+
payer: LocalAccount;
|
|
53
|
+
};
|
|
54
|
+
export type Scheme = {
|
|
55
|
+
name: string;
|
|
56
|
+
/** Whether this scheme can pay the requirement at all (scope and budget are checked separately). */
|
|
57
|
+
supports(req: PaymentRequirements, ctx: SchemeContext): boolean;
|
|
58
|
+
/** Produces the scheme-specific payload. */
|
|
59
|
+
pay(req: PaymentRequirements, ctx: SchemeContext): Promise<unknown>;
|
|
60
|
+
/** Extra request headers some bindings need, computed over the final PAYMENT-SIGNATURE value. */
|
|
61
|
+
headers?(paymentHeader: string, url: URL): Record<string, string>;
|
|
62
|
+
};
|
|
63
|
+
/** exact / EIP-3009: the payer signs a transferWithAuthorization the facilitator submits. */
|
|
64
|
+
export declare const exactEip3009: Scheme;
|
|
65
|
+
export declare const PERMIT2_ADDRESS: Address;
|
|
66
|
+
/** The spender x402 requires for Permit2 payments: the proxy, never the facilitator. */
|
|
67
|
+
export declare const X402_PERMIT2_PROXY: Address;
|
|
68
|
+
export declare const PERMIT2_WITNESS_TYPES: {
|
|
69
|
+
readonly PermitWitnessTransferFrom: readonly [{
|
|
70
|
+
readonly name: 'permitted';
|
|
71
|
+
readonly type: 'TokenPermissions';
|
|
72
|
+
}, {
|
|
73
|
+
readonly name: 'spender';
|
|
74
|
+
readonly type: 'address';
|
|
75
|
+
}, {
|
|
76
|
+
readonly name: 'nonce';
|
|
77
|
+
readonly type: 'uint256';
|
|
78
|
+
}, {
|
|
79
|
+
readonly name: 'deadline';
|
|
80
|
+
readonly type: 'uint256';
|
|
81
|
+
}, {
|
|
82
|
+
readonly name: 'witness';
|
|
83
|
+
readonly type: 'Witness';
|
|
84
|
+
}];
|
|
85
|
+
readonly TokenPermissions: readonly [{
|
|
86
|
+
readonly name: 'token';
|
|
87
|
+
readonly type: 'address';
|
|
88
|
+
}, {
|
|
89
|
+
readonly name: 'amount';
|
|
90
|
+
readonly type: 'uint256';
|
|
91
|
+
}];
|
|
92
|
+
readonly Witness: readonly [{
|
|
93
|
+
readonly name: 'to';
|
|
94
|
+
readonly type: 'address';
|
|
95
|
+
}, {
|
|
96
|
+
readonly name: 'validAfter';
|
|
97
|
+
readonly type: 'uint256';
|
|
98
|
+
}];
|
|
99
|
+
};
|
|
100
|
+
/** exact / Permit2: a witness transfer that can only pay `payTo`, through the x402 proxy. */
|
|
101
|
+
export declare const exactPermit2: Scheme;
|
|
102
|
+
/**
|
|
103
|
+
* batch-settlement on the cloudflare:402 network: the only published binding of that scheme.
|
|
104
|
+
* Credit-backed and offchain: the client commits to an amount in fiat, signs the request with an
|
|
105
|
+
* Ed25519 HTTP Message Signature (RFC 9421, Web Bot Auth), and Cloudflare bills the registered
|
|
106
|
+
* account later. No token moves and no chain is involved, so nothing here is bounded onchain: the
|
|
107
|
+
* only OATH limit that applies is the service's maxSpend, counted in the requirement's own units
|
|
108
|
+
* (cents for USD), enforced by this adapter.
|
|
109
|
+
* Needs a Cloudflare account with the signature agent registered.
|
|
110
|
+
*/
|
|
111
|
+
export declare function cloudflareBatchSettlement(opts: {
|
|
112
|
+
signatureAgent: string;
|
|
113
|
+
keyId: string;
|
|
114
|
+
privateKey: KeyObject | string;
|
|
115
|
+
validForSeconds?: number;
|
|
116
|
+
}): Scheme;
|
|
117
|
+
export type X402Options = {
|
|
118
|
+
oath: Oath;
|
|
119
|
+
/** Signs payments. For the onchain schemes it holds the balance payments are made from. */
|
|
120
|
+
payer: LocalAccount;
|
|
121
|
+
/** Maps a URL to a service id in the OATH. Defaults to the URL's host. */
|
|
122
|
+
serviceId?: (url: URL) => string;
|
|
123
|
+
/** Tried in order for each accepted requirement. Defaults to EIP-3009, then Permit2. */
|
|
124
|
+
schemes?: Scheme[];
|
|
125
|
+
fetch?: typeof fetch;
|
|
126
|
+
};
|
|
127
|
+
export declare function createX402(opts: X402Options): {
|
|
128
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
129
|
+
pay: (required: PaymentRequired, serviceId: string, url?: string) => Promise<{
|
|
130
|
+
paymentPayload: PaymentPayload;
|
|
131
|
+
accepted: PaymentRequirements;
|
|
132
|
+
scheme: Scheme;
|
|
133
|
+
settled: (settlement: unknown) => Payment;
|
|
134
|
+
}>;
|
|
135
|
+
/** Every payment a service accepted. */
|
|
136
|
+
payments: Payment[];
|
|
137
|
+
spent: (serviceId?: string) => bigint;
|
|
138
|
+
/**
|
|
139
|
+
* The transfer that funds the payer's float: an ordinary token transfer from the OATH Account, to
|
|
140
|
+
* go in the workflow before the paid calls. Fund what the plan's payments add up to and no more;
|
|
141
|
+
* the account measures it against the OATH's spend limit like any other outflow.
|
|
142
|
+
*/
|
|
143
|
+
float: (asset: Address, amount: bigint, id?: string) => ActionInput;
|
|
144
|
+
/**
|
|
145
|
+
* Sends whatever is left of the float back to the OATH Account. Run it when the workflow ends, so
|
|
146
|
+
* no balance is left sitting on the agent key. The payer pays the gas for this one transaction.
|
|
147
|
+
* Returned funds do not restore the OATH's spend allowance: the limit counts what left the account.
|
|
148
|
+
*/
|
|
149
|
+
sweep(chain: {
|
|
150
|
+
public: {
|
|
151
|
+
readContract: (a: any) => Promise<unknown>;
|
|
152
|
+
waitForTransactionReceipt: (a: {
|
|
153
|
+
hash: Hex;
|
|
154
|
+
}) => Promise<unknown>;
|
|
155
|
+
};
|
|
156
|
+
wallet: (account: LocalAccount) => {
|
|
157
|
+
writeContract: (a: any) => Promise<Hex>;
|
|
158
|
+
};
|
|
159
|
+
}, asset: Address): Promise<{
|
|
160
|
+
amount: bigint;
|
|
161
|
+
txHash?: Hex;
|
|
162
|
+
}>;
|
|
163
|
+
/** Token payments as X402_PAYMENT nodes, for the Builder's graph and receipts. */
|
|
164
|
+
actions: () => ActionInput[];
|
|
165
|
+
};
|
|
166
|
+
export type X402 = ReturnType<typeof createX402>;
|
|
167
|
+
/** Wraps fetch. Shorthand for createX402(opts).fetch. */
|
|
168
|
+
export declare const withX402: (fetchImpl: typeof fetch, opts: Omit<X402Options, 'fetch'>) => (input: string | URL, init?: RequestInit) => Promise<Response>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// @oathbuild/x402 — pays x402 V2 services inside an OATH's budget.
|
|
2
|
+
//
|
|
3
|
+
// Uses x402 as specified: PAYMENT-REQUIRED / PAYMENT-SIGNATURE / PAYMENT-RESPONSE headers
|
|
4
|
+
// (base64 JSON) over HTTP, and _meta["x402/payment"] over MCP. No OATH-specific headers.
|
|
5
|
+
//
|
|
6
|
+
// Schemes:
|
|
7
|
+
// exact + EIP-3009 built in
|
|
8
|
+
// exact + Permit2 built in (the payer must have approved Permit2 once)
|
|
9
|
+
// batch-settlement, cloudflare:402 opt in with cloudflareBatchSettlement(...)
|
|
10
|
+
//
|
|
11
|
+
// Funding model for the onchain schemes: payments are signed by the agent's own key from a balance
|
|
12
|
+
// the agent holds. That balance is topped up by an ordinary transfer inside an OATH bundle, which the
|
|
13
|
+
// account measures against the spend limit. So the onchain bound on x402 spend is the amount moved to
|
|
14
|
+
// the agent; the per-service caps below are enforced here, by the adapter.
|
|
15
|
+
import { sign as edSign, createPrivateKey } from 'node:crypto';
|
|
16
|
+
import { toHex, hexToBigInt, isAddress, encodeFunctionData, erc20Abi } from 'viem';
|
|
17
|
+
export class X402Error extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message) { super(message); this.code = code; }
|
|
20
|
+
}
|
|
21
|
+
const b64encode = (v) => Buffer.from(JSON.stringify(v)).toString('base64');
|
|
22
|
+
const b64decode = (s) => JSON.parse(Buffer.from(s, 'base64').toString('utf8'));
|
|
23
|
+
export const decodePaymentRequired = (header) => b64decode(header);
|
|
24
|
+
const randomHex = (bytes) => toHex(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
25
|
+
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
|
26
|
+
const onChain = (req, ctx) => req.scheme === 'exact' && req.network === `eip155:${ctx.oath.chainId}` && isAddress(req.asset);
|
|
27
|
+
const deadline = (req, ctx) => Math.min(nowSeconds() + req.maxTimeoutSeconds, Number(ctx.oath.validUntil));
|
|
28
|
+
/** exact / EIP-3009: the payer signs a transferWithAuthorization the facilitator submits. */
|
|
29
|
+
export const exactEip3009 = {
|
|
30
|
+
name: 'exact/eip3009',
|
|
31
|
+
supports: (req, ctx) => onChain(req, ctx) && (req.extra?.assetTransferMethod ?? 'eip3009') === 'eip3009',
|
|
32
|
+
async pay(req, ctx) {
|
|
33
|
+
const authorization = { from: ctx.payer.address, to: req.payTo, value: req.amount, validAfter: '0', validBefore: String(deadline(req, ctx)), nonce: randomHex(32) };
|
|
34
|
+
const signature = await ctx.payer.signTypedData({
|
|
35
|
+
domain: { name: req.extra?.name, version: req.extra?.version, chainId: Number(ctx.oath.chainId), verifyingContract: req.asset },
|
|
36
|
+
types: { TransferWithAuthorization: [
|
|
37
|
+
{ name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' },
|
|
38
|
+
{ name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' },
|
|
39
|
+
] },
|
|
40
|
+
primaryType: 'TransferWithAuthorization',
|
|
41
|
+
message: { from: authorization.from, to: req.payTo, value: BigInt(req.amount), validAfter: 0n, validBefore: BigInt(authorization.validBefore), nonce: authorization.nonce },
|
|
42
|
+
});
|
|
43
|
+
return { signature, authorization };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
|
|
47
|
+
/** The spender x402 requires for Permit2 payments: the proxy, never the facilitator. */
|
|
48
|
+
export const X402_PERMIT2_PROXY = '0x402085c248EeA27D92E8b30b2C58ed07f9E20001';
|
|
49
|
+
export const PERMIT2_WITNESS_TYPES = {
|
|
50
|
+
PermitWitnessTransferFrom: [
|
|
51
|
+
{ name: 'permitted', type: 'TokenPermissions' }, { name: 'spender', type: 'address' },
|
|
52
|
+
{ name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, { name: 'witness', type: 'Witness' },
|
|
53
|
+
],
|
|
54
|
+
TokenPermissions: [{ name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }],
|
|
55
|
+
Witness: [{ name: 'to', type: 'address' }, { name: 'validAfter', type: 'uint256' }],
|
|
56
|
+
};
|
|
57
|
+
/** exact / Permit2: a witness transfer that can only pay `payTo`, through the x402 proxy. */
|
|
58
|
+
export const exactPermit2 = {
|
|
59
|
+
name: 'exact/permit2',
|
|
60
|
+
supports: (req, ctx) => onChain(req, ctx) && req.extra?.assetTransferMethod === 'permit2',
|
|
61
|
+
async pay(req, ctx) {
|
|
62
|
+
const permit2Authorization = {
|
|
63
|
+
permitted: { token: req.asset, amount: req.amount },
|
|
64
|
+
from: ctx.payer.address,
|
|
65
|
+
spender: X402_PERMIT2_PROXY,
|
|
66
|
+
nonce: hexToBigInt(randomHex(32)).toString(), // Permit2 nonces are unordered: any unused value
|
|
67
|
+
deadline: String(deadline(req, ctx)),
|
|
68
|
+
witness: { to: req.payTo, validAfter: String(nowSeconds() - 60) },
|
|
69
|
+
};
|
|
70
|
+
const signature = await ctx.payer.signTypedData({
|
|
71
|
+
domain: { name: 'Permit2', chainId: Number(ctx.oath.chainId), verifyingContract: PERMIT2_ADDRESS },
|
|
72
|
+
types: PERMIT2_WITNESS_TYPES,
|
|
73
|
+
primaryType: 'PermitWitnessTransferFrom',
|
|
74
|
+
message: {
|
|
75
|
+
permitted: { token: req.asset, amount: BigInt(req.amount) }, spender: X402_PERMIT2_PROXY,
|
|
76
|
+
nonce: BigInt(permit2Authorization.nonce), deadline: BigInt(permit2Authorization.deadline),
|
|
77
|
+
witness: { to: req.payTo, validAfter: BigInt(permit2Authorization.witness.validAfter) },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
return { signature, permit2Authorization };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* batch-settlement on the cloudflare:402 network: the only published binding of that scheme.
|
|
85
|
+
* Credit-backed and offchain: the client commits to an amount in fiat, signs the request with an
|
|
86
|
+
* Ed25519 HTTP Message Signature (RFC 9421, Web Bot Auth), and Cloudflare bills the registered
|
|
87
|
+
* account later. No token moves and no chain is involved, so nothing here is bounded onchain: the
|
|
88
|
+
* only OATH limit that applies is the service's maxSpend, counted in the requirement's own units
|
|
89
|
+
* (cents for USD), enforced by this adapter.
|
|
90
|
+
* Needs a Cloudflare account with the signature agent registered.
|
|
91
|
+
*/
|
|
92
|
+
export function cloudflareBatchSettlement(opts) {
|
|
93
|
+
const key = typeof opts.privateKey === 'string' ? createPrivateKey(opts.privateKey) : opts.privateKey;
|
|
94
|
+
return {
|
|
95
|
+
name: 'batch-settlement/cloudflare',
|
|
96
|
+
supports: (req) => req.scheme === 'batch-settlement' && req.network === 'cloudflare:402',
|
|
97
|
+
pay: async (req) => ({ amount: req.amount, asset: req.asset }),
|
|
98
|
+
headers(paymentHeader, url) {
|
|
99
|
+
const created = nowSeconds(), expires = created + (opts.validForSeconds ?? 300);
|
|
100
|
+
const params = `("@authority" "signature-agent" "payment-signature");created=${created};expires=${expires};keyid="${opts.keyId}";tag="web-bot-auth"`;
|
|
101
|
+
const base = [`"@authority": ${url.host}`, `"signature-agent": ${opts.signatureAgent}`, `"payment-signature": ${paymentHeader}`, `"@signature-params": ${params}`].join('\n');
|
|
102
|
+
return { 'Signature-Agent': opts.signatureAgent, 'Signature-Input': `sig=${params}`, Signature: `sig=:${edSign(null, Buffer.from(base), key).toString('base64')}:` };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function createX402(opts) {
|
|
107
|
+
const { oath, payer } = opts;
|
|
108
|
+
const baseFetch = opts.fetch ?? fetch;
|
|
109
|
+
const schemes = opts.schemes ?? [exactEip3009, exactPermit2];
|
|
110
|
+
const ctx = { oath, payer };
|
|
111
|
+
const payments = [];
|
|
112
|
+
const sum = (f) => payments.filter(f).reduce((n, p) => n + p.amount, 0n);
|
|
113
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
114
|
+
/** Picks a requirement a scheme can pay and the OATH allows. Throws with the reason otherwise. */
|
|
115
|
+
function select(required, serviceId) {
|
|
116
|
+
if (nowSeconds() >= Number(oath.validUntil))
|
|
117
|
+
throw new X402Error('OATH_EXPIRED', 'The OATH has expired.');
|
|
118
|
+
const service = oath.services.find((s) => s.serviceId === serviceId);
|
|
119
|
+
if (!service)
|
|
120
|
+
throw new X402Error('SERVICE_NOT_ALLOWED', `Service "${serviceId}" is not in the OATH.`);
|
|
121
|
+
const payable = required.accepts.flatMap((accepted) => { const scheme = schemes.find((s) => s.supports(accepted, ctx)); return scheme ? [{ accepted, scheme }] : []; });
|
|
122
|
+
if (!payable.length)
|
|
123
|
+
throw new X402Error('NO_SUPPORTED_SCHEME', `No accepted scheme is supported (configured: ${schemes.map((s) => s.name).join(', ')}).`);
|
|
124
|
+
let reason;
|
|
125
|
+
for (const option of payable) {
|
|
126
|
+
const r = option.accepted, amount = BigInt(r.amount);
|
|
127
|
+
// The service cap counts in the asset's own units. One service is expected to bill in one asset.
|
|
128
|
+
if (sum((p) => p.serviceId === serviceId && same(p.asset, r.asset)) + amount > service.maxSpend) {
|
|
129
|
+
reason = new X402Error('SPEND_LIMIT_EXCEEDED', `Payment would exceed the cap for "${serviceId}".`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (isAddress(r.asset)) { // a token: also bounded by the OATH's spend limit for that token
|
|
133
|
+
const limit = oath.spendLimits.find((l) => same(l.asset, r.asset));
|
|
134
|
+
if (!limit) {
|
|
135
|
+
reason = new X402Error('ASSET_NOT_ALLOWED', `Asset ${r.asset} has no spend limit in the OATH.`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (sum((p) => same(p.asset, r.asset)) + amount > limit.amount) {
|
|
139
|
+
reason = new X402Error('SPEND_LIMIT_EXCEEDED', `Payment would exceed the OATH spend limit for ${r.asset}.`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return option;
|
|
144
|
+
}
|
|
145
|
+
throw reason;
|
|
146
|
+
}
|
|
147
|
+
/** Authorizes one payment for a requirement. Nothing is signed unless the OATH allows it.
|
|
148
|
+
* Call `settled` once the service has accepted it; only then does it count as spent. */
|
|
149
|
+
async function pay(required, serviceId, url = required.resource?.url ?? '') {
|
|
150
|
+
const { accepted, scheme } = select(required, serviceId);
|
|
151
|
+
const paymentPayload = { x402Version: 2, resource: required.resource, accepted, payload: await scheme.pay(accepted, ctx) };
|
|
152
|
+
const settled = (settlement) => {
|
|
153
|
+
const payment = { serviceId, url, scheme: scheme.name, network: accepted.network, asset: accepted.asset, amount: BigInt(accepted.amount), payTo: accepted.payTo, settlement, timestamp: nowSeconds() };
|
|
154
|
+
payments.push(payment);
|
|
155
|
+
return payment;
|
|
156
|
+
};
|
|
157
|
+
return { paymentPayload, accepted, scheme, settled };
|
|
158
|
+
}
|
|
159
|
+
/** fetch that answers 402 Payment Required once, if the OATH allows the payment. */
|
|
160
|
+
async function paidFetch(input, init = {}) {
|
|
161
|
+
const url = new URL(String(input));
|
|
162
|
+
const first = await baseFetch(url, init);
|
|
163
|
+
const header = first.headers.get('PAYMENT-REQUIRED');
|
|
164
|
+
if (first.status !== 402 || !header)
|
|
165
|
+
return first;
|
|
166
|
+
const serviceId = (opts.serviceId ?? ((u) => u.host))(url);
|
|
167
|
+
const { paymentPayload, scheme, settled } = await pay(decodePaymentRequired(header), serviceId, url.href);
|
|
168
|
+
const paymentHeader = b64encode(paymentPayload);
|
|
169
|
+
const headers = new Headers(init.headers);
|
|
170
|
+
headers.set('PAYMENT-SIGNATURE', paymentHeader);
|
|
171
|
+
for (const [k, v] of Object.entries(scheme.headers?.(paymentHeader, url) ?? {}))
|
|
172
|
+
headers.set(k, v);
|
|
173
|
+
const second = await baseFetch(url, { ...init, headers });
|
|
174
|
+
// A service that still answers 402 did not accept the payment; nothing is recorded as spent.
|
|
175
|
+
if (second.status !== 402) {
|
|
176
|
+
const response = second.headers.get('PAYMENT-RESPONSE');
|
|
177
|
+
settled(response ? b64decode(response) : null);
|
|
178
|
+
}
|
|
179
|
+
return second;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
fetch: paidFetch,
|
|
183
|
+
pay,
|
|
184
|
+
/** Every payment a service accepted. */
|
|
185
|
+
payments,
|
|
186
|
+
spent: (serviceId) => sum((p) => !serviceId || p.serviceId === serviceId),
|
|
187
|
+
/**
|
|
188
|
+
* The transfer that funds the payer's float: an ordinary token transfer from the OATH Account, to
|
|
189
|
+
* go in the workflow before the paid calls. Fund what the plan's payments add up to and no more;
|
|
190
|
+
* the account measures it against the OATH's spend limit like any other outflow.
|
|
191
|
+
*/
|
|
192
|
+
float: (asset, amount, id = 'x402-float') => ({
|
|
193
|
+
id, type: 'TOKEN_TRANSFER', label: 'fund x402 float', target: asset, spend: { asset, amount },
|
|
194
|
+
payload: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [payer.address, amount] }),
|
|
195
|
+
}),
|
|
196
|
+
/**
|
|
197
|
+
* Sends whatever is left of the float back to the OATH Account. Run it when the workflow ends, so
|
|
198
|
+
* no balance is left sitting on the agent key. The payer pays the gas for this one transaction.
|
|
199
|
+
* Returned funds do not restore the OATH's spend allowance: the limit counts what left the account.
|
|
200
|
+
*/
|
|
201
|
+
async sweep(chain, asset) {
|
|
202
|
+
const amount = await chain.public.readContract({ address: asset, abi: erc20Abi, functionName: 'balanceOf', args: [payer.address] });
|
|
203
|
+
if (amount === 0n)
|
|
204
|
+
return { amount };
|
|
205
|
+
const txHash = await chain.wallet(payer).writeContract({ address: asset, abi: erc20Abi, functionName: 'transfer', args: [oath.account, amount], chain: null, account: payer });
|
|
206
|
+
await chain.public.waitForTransactionReceipt({ hash: txHash });
|
|
207
|
+
return { amount, txHash };
|
|
208
|
+
},
|
|
209
|
+
/** Token payments as X402_PAYMENT nodes, for the Builder's graph and receipts. */
|
|
210
|
+
actions: () => payments.map((p, i) => ({
|
|
211
|
+
id: `x402-${i + 1}`, type: 'X402_PAYMENT', label: `x402 ${p.serviceId}`, target: p.serviceId, scheme: p.scheme.startsWith('batch') ? 'batch' : 'exact',
|
|
212
|
+
...(isAddress(p.asset) ? { spend: { asset: p.asset, amount: p.amount } } : {}),
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** Wraps fetch. Shorthand for createX402(opts).fetch. */
|
|
217
|
+
export const withX402 = (fetchImpl, opts) => createX402({ ...opts, fetch: fetchImpl }).fetch;
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oathbuild/x402",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OATH x402 adapter: pay x402 services inside the budget a signed OATH allows.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"oath",
|
|
7
|
+
"agents",
|
|
8
|
+
"transaction-builder",
|
|
9
|
+
"mcp",
|
|
10
|
+
"x402",
|
|
11
|
+
"robinhood-chain",
|
|
12
|
+
"eip-712"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://oathbuild.net/docs",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"!dist/*.tsbuildinfo",
|
|
26
|
+
"!dist/.tsbuildinfo"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@oathbuild/core": "0.1.0",
|
|
33
|
+
"viem": "^2.21.0"
|
|
34
|
+
}
|
|
6
35
|
}
|