@agenttoll/sdk 1.0.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 +48 -0
- package/agenttoll-tool-schema.json +28 -0
- package/agenttoll-tool.js +181 -0
- package/package.json +41 -0
- package/tollbooth-edge.js +134 -0
- package/tollbooth.js +292 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# AgentToll SDK
|
|
2
|
+
|
|
3
|
+
One-line micropayment integration for AI agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### From npm (recommended)
|
|
8
|
+
```bash
|
|
9
|
+
npm install @agenttoll/sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### From GitHub (if npm is unavailable)
|
|
13
|
+
```bash
|
|
14
|
+
npm install github:agenttoll/sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Direct download
|
|
18
|
+
Copy [tollbooth.js](./tollbooth.js) into your project.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Express
|
|
23
|
+
```javascript
|
|
24
|
+
import tollbooth from '@agenttoll/sdk';
|
|
25
|
+
// or if installed locally:
|
|
26
|
+
// import tollbooth from './tollbooth.js';
|
|
27
|
+
|
|
28
|
+
app.use(tollbooth('pk_live_xxx'));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Cloudflare Workers
|
|
32
|
+
```javascript
|
|
33
|
+
import { tollgate } from '@agenttoll/sdk/edge';
|
|
34
|
+
|
|
35
|
+
export default tollgate('pk_live_xxx', { amount: 0.005 });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Agent Tool (for AI agents)
|
|
39
|
+
```javascript
|
|
40
|
+
import { payTollTool } from '@agenttoll/sdk/tool';
|
|
41
|
+
|
|
42
|
+
// Add to your agent's tools
|
|
43
|
+
tools: [payTollTool]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API Reference
|
|
47
|
+
|
|
48
|
+
See [https://agenttoll.io/docs](https://agenttoll.io/docs) for full documentation.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pay_toll",
|
|
3
|
+
"description": "Pay a micropayment toll to access a paywalled resource. Use this when you encounter an HTTP 402 Payment Required response. The tool handles the payment via Solana USDC and returns an access token for the resource.",
|
|
4
|
+
"strict": true,
|
|
5
|
+
"parameters": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["url", "amount"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"url": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The URL of the resource that requires payment (from the 402 response)"
|
|
12
|
+
},
|
|
13
|
+
"amount": {
|
|
14
|
+
"type": "number",
|
|
15
|
+
"description": "The payment amount in USDC (from the 402 response x402 headers or body)"
|
|
16
|
+
},
|
|
17
|
+
"max_amount": {
|
|
18
|
+
"type": "number",
|
|
19
|
+
"description": "Maximum amount you're willing to pay. If the requested amount exceeds this, the tool will not proceed. Default: 0.05 USDC"
|
|
20
|
+
},
|
|
21
|
+
"reason": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Why accessing this resource is valuable for your current task (helps with logging and human oversight)"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentToll Tool: pay_toll
|
|
3
|
+
*
|
|
4
|
+
* This tool allows AgentToll/OpenClaw agents to autonomously pay
|
|
5
|
+
* micropayment tolls when they hit 402 Payment Required responses.
|
|
6
|
+
*
|
|
7
|
+
* Installation:
|
|
8
|
+
* Add this tool to your AgentToll's tool configuration
|
|
9
|
+
*
|
|
10
|
+
* Usage in agent prompts:
|
|
11
|
+
* "If you encounter a 402 Payment Required error on a useful resource,
|
|
12
|
+
* use the pay_toll tool to pay up to 0.01 USDC if the content is
|
|
13
|
+
* valuable for the current task."
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Keypair, Connection, PublicKey, Transaction } from '@solana/web3.js';
|
|
17
|
+
import {
|
|
18
|
+
getAssociatedTokenAddress,
|
|
19
|
+
createTransferInstruction,
|
|
20
|
+
TOKEN_PROGRAM_ID
|
|
21
|
+
} from '@solana/spl-token';
|
|
22
|
+
|
|
23
|
+
const TOLL_API = 'https://toll.agenttoll.io';
|
|
24
|
+
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); // Mainnet USDC
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AgentToll-compatible tool implementation
|
|
28
|
+
*/
|
|
29
|
+
export async function pay_toll(params, context) {
|
|
30
|
+
const { url, amount, max_amount = 0.05, reason } = params;
|
|
31
|
+
const { wallet_keypair, solana_rpc } = context;
|
|
32
|
+
|
|
33
|
+
// Safety check: don't overpay
|
|
34
|
+
if (amount > max_amount) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
error: `Requested amount ${amount} USDC exceeds max_amount ${max_amount} USDC`,
|
|
38
|
+
suggestion: 'Increase max_amount if this resource is valuable, or skip this resource',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// 1. Get payment details from toll service
|
|
44
|
+
const quoteRes = await fetch(`${TOLL_API}/api/pay/quote?resource=${encodeURIComponent(url)}&amount=${amount}`);
|
|
45
|
+
const quote = await quoteRes.json();
|
|
46
|
+
|
|
47
|
+
// 2. Create and send Solana USDC transfer
|
|
48
|
+
const connection = new Connection(solana_rpc || 'https://api.mainnet-beta.solana.com');
|
|
49
|
+
const payer = Keypair.fromSecretKey(wallet_keypair);
|
|
50
|
+
|
|
51
|
+
const payerAta = await getAssociatedTokenAddress(USDC_MINT, payer.publicKey);
|
|
52
|
+
const receiverAta = await getAssociatedTokenAddress(USDC_MINT, new PublicKey(quote.receiver_wallet));
|
|
53
|
+
|
|
54
|
+
// Amount in USDC (6 decimals)
|
|
55
|
+
const transferAmount = Math.floor(amount * 1_000_000);
|
|
56
|
+
|
|
57
|
+
const transaction = new Transaction().add(
|
|
58
|
+
createTransferInstruction(
|
|
59
|
+
payerAta,
|
|
60
|
+
receiverAta,
|
|
61
|
+
payer.publicKey,
|
|
62
|
+
transferAmount,
|
|
63
|
+
[],
|
|
64
|
+
TOKEN_PROGRAM_ID
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const signature = await connection.sendTransaction(transaction, [payer]);
|
|
69
|
+
await connection.confirmTransaction(signature, 'confirmed');
|
|
70
|
+
|
|
71
|
+
// 3. Submit payment proof to toll service
|
|
72
|
+
const payRes = await fetch(`${TOLL_API}/api/pay`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
publisher: quote.publisher || 'default',
|
|
77
|
+
amount: amount,
|
|
78
|
+
resource: url,
|
|
79
|
+
tx_signature: signature,
|
|
80
|
+
agent_id: context.agent_id || 'agenttoll',
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const payResult = await payRes.json();
|
|
85
|
+
|
|
86
|
+
if (!payResult.success) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: payResult.error,
|
|
90
|
+
tx_signature: signature,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. Return token for agent to use
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
token: payResult.token,
|
|
98
|
+
expires_in: payResult.expires_in,
|
|
99
|
+
amount_paid: amount,
|
|
100
|
+
tx_signature: signature,
|
|
101
|
+
retry_headers: {
|
|
102
|
+
'Authorization': `Bearer ${payResult.token}`,
|
|
103
|
+
},
|
|
104
|
+
instructions: `Payment successful. Retry the original request to ${url} with the Authorization header set to: Bearer ${payResult.token}`,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: error.message,
|
|
111
|
+
suggestion: 'Check wallet balance or try again later',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse 402 response to extract payment details
|
|
118
|
+
* Helper for agents to understand 402 responses
|
|
119
|
+
*/
|
|
120
|
+
export function parse402Response(response) {
|
|
121
|
+
// Try x402 headers first
|
|
122
|
+
const headers = response.headers || {};
|
|
123
|
+
if (headers['x-402-amount']) {
|
|
124
|
+
return {
|
|
125
|
+
amount: parseFloat(headers['x-402-amount']),
|
|
126
|
+
currency: headers['x-402-currency'] || 'USDC',
|
|
127
|
+
pay_url: headers['x-402-pay-url'],
|
|
128
|
+
network: headers['x-402-network'] || 'solana',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try body
|
|
133
|
+
const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
|
|
134
|
+
if (body?.x402) {
|
|
135
|
+
return {
|
|
136
|
+
amount: body.x402.amount,
|
|
137
|
+
currency: body.x402.currency,
|
|
138
|
+
network: body.x402.network,
|
|
139
|
+
pay_url: body.payment?.pay_url,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (body?.payment) {
|
|
144
|
+
return {
|
|
145
|
+
amount: body.payment.amount,
|
|
146
|
+
currency: body.payment.currency,
|
|
147
|
+
network: body.payment.network,
|
|
148
|
+
pay_url: body.payment.pay_url,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* AgentToll system prompt addition for toll handling
|
|
157
|
+
*/
|
|
158
|
+
export const TOLL_SYSTEM_PROMPT = `
|
|
159
|
+
## Toll Payment Capability
|
|
160
|
+
|
|
161
|
+
You have access to the pay_toll tool for handling paywalled content. When you encounter an HTTP 402 Payment Required response:
|
|
162
|
+
|
|
163
|
+
1. Check the x402 headers or response body for payment details (amount, currency, network)
|
|
164
|
+
2. Evaluate if the resource is worth paying for based on your current task
|
|
165
|
+
3. If valuable and within your budget (default max 0.01 USDC), use pay_toll to pay
|
|
166
|
+
4. Retry the original request with the returned Authorization token
|
|
167
|
+
|
|
168
|
+
Example 402 handling:
|
|
169
|
+
- Receive 402 with amount: 0.005 USDC
|
|
170
|
+
- Decide: "This API has data I need for my research task"
|
|
171
|
+
- Call: pay_toll(url="...", amount=0.005, reason="Need this data for research")
|
|
172
|
+
- Retry request with: Authorization: Bearer <returned_token>
|
|
173
|
+
|
|
174
|
+
Budget guidelines:
|
|
175
|
+
- Small data requests: up to 0.005 USDC
|
|
176
|
+
- API calls: up to 0.01 USDC
|
|
177
|
+
- Premium content: up to 0.05 USDC (ask human first if possible)
|
|
178
|
+
- Never exceed 0.1 USDC without explicit human approval
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
export default { pay_toll, parse402Response, TOLL_SYSTEM_PROMPT };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agenttoll/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "One-line micropayment integration for AI agents - Express, Cloudflare Workers, Vercel Edge",
|
|
5
|
+
"main": "tollbooth.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./tollbooth.js",
|
|
8
|
+
"./edge": "./tollbooth-edge.js",
|
|
9
|
+
"./next": "./tollbooth-edge.js",
|
|
10
|
+
"./tool": "./agenttoll-tool.js"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agenttoll",
|
|
15
|
+
"openclaw",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"micropayments",
|
|
18
|
+
"x402",
|
|
19
|
+
"solana",
|
|
20
|
+
"base",
|
|
21
|
+
"tollbooth",
|
|
22
|
+
"express-middleware",
|
|
23
|
+
"cloudflare-workers"
|
|
24
|
+
],
|
|
25
|
+
"author": "AgentToll",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/agenttoll/sdk"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://agenttoll.io",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"tollbooth.js",
|
|
37
|
+
"tollbooth-edge.js",
|
|
38
|
+
"agenttoll-tool.js",
|
|
39
|
+
"agenttoll-tool-schema.json"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentToll - Edge Runtime SDK
|
|
3
|
+
* For Cloudflare Workers, Vercel Edge, Deno Deploy
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { tollgate } from '@agenttoll/sdk/edge'
|
|
7
|
+
* export default tollgate('your-api-key')
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TOLL_API_BASE = 'https://toll.agenttoll.io';
|
|
11
|
+
|
|
12
|
+
const AGENT_PATTERNS = [
|
|
13
|
+
/agenttoll/i, /openclaw/i, /clawd/i, /openai-agent/i,
|
|
14
|
+
/anthropic-agent/i, /autogpt/i, /agentgpt/i, /babyagi/i,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function isAgent(request) {
|
|
18
|
+
const ua = request.headers.get('user-agent') || '';
|
|
19
|
+
const agentHeader = request.headers.get('x-agent-type') || request.headers.get('x-agenttoll-id') || '';
|
|
20
|
+
if (AGENT_PATTERNS.some(p => p.test(ua))) return true;
|
|
21
|
+
if (agentHeader) return true;
|
|
22
|
+
if (request.headers.get('x-402-capable') === 'true') return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function verifyToken(token, apiKey) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${TOLL_API_BASE}/api/verify`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json', 'X-Publisher-Key': apiKey },
|
|
31
|
+
body: JSON.stringify({ token }),
|
|
32
|
+
});
|
|
33
|
+
return (await res.json()).valid === true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function build402(request, apiKey, amount) {
|
|
40
|
+
const url = new URL(request.url);
|
|
41
|
+
return {
|
|
42
|
+
status: 402,
|
|
43
|
+
message: 'Payment Required',
|
|
44
|
+
agent_instructions: `Pay ${amount} USDC to access. Use returned token in Authorization header.`,
|
|
45
|
+
payment: {
|
|
46
|
+
amount,
|
|
47
|
+
currency: 'USDC',
|
|
48
|
+
network: 'solana',
|
|
49
|
+
pay_url: `${TOLL_API_BASE}/pay?publisher=${apiKey}&amount=${amount}&resource=${encodeURIComponent(url.href)}`,
|
|
50
|
+
api_endpoint: `${TOLL_API_BASE}/api/pay`,
|
|
51
|
+
},
|
|
52
|
+
x402: { version: 1, amount, currency: 'USDC', network: 'solana-mainnet' },
|
|
53
|
+
retry: { method: request.method, url: url.href, headers: { 'Authorization': 'Bearer <token>' } },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Edge-compatible tollgate wrapper
|
|
59
|
+
* Wraps your handler and gates it behind payment
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Cloudflare Worker
|
|
63
|
+
* import { tollgate } from '@agenttoll/sdk/edge'
|
|
64
|
+
*
|
|
65
|
+
* export default tollgate('pk_live_xxx', {
|
|
66
|
+
* amount: 0.005,
|
|
67
|
+
* freeForHumans: true,
|
|
68
|
+
* handler: async (request) => {
|
|
69
|
+
* return new Response('Premium content!')
|
|
70
|
+
* }
|
|
71
|
+
* })
|
|
72
|
+
*/
|
|
73
|
+
export function tollgate(apiKey, options = {}) {
|
|
74
|
+
const amount = options.amount || 0.005;
|
|
75
|
+
const freeForHumans = options.freeForHumans ?? false;
|
|
76
|
+
const handler = options.handler;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
async fetch(request, env, ctx) {
|
|
80
|
+
const agent = isAgent(request);
|
|
81
|
+
|
|
82
|
+
// Free for humans if configured
|
|
83
|
+
if (freeForHumans && !agent) {
|
|
84
|
+
return handler ? handler(request, env, ctx) : new Response('OK');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check payment token
|
|
88
|
+
const auth = request.headers.get('authorization') || '';
|
|
89
|
+
const token = auth.replace(/^Bearer\s+/i, '');
|
|
90
|
+
|
|
91
|
+
if (token && await verifyToken(token, apiKey)) {
|
|
92
|
+
return handler ? handler(request, env, ctx) : new Response('OK');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Return 402
|
|
96
|
+
const paymentInfo = build402(request, apiKey, amount);
|
|
97
|
+
return new Response(JSON.stringify(paymentInfo), {
|
|
98
|
+
status: 402,
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
'X-402-Version': '1',
|
|
102
|
+
'X-402-Amount': amount.toString(),
|
|
103
|
+
'X-402-Currency': 'USDC',
|
|
104
|
+
'X-402-Pay-URL': paymentInfo.payment.pay_url,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Middleware for edge frameworks (Hono, itty-router, etc.)
|
|
113
|
+
*/
|
|
114
|
+
export function tollMiddleware(apiKey, options = {}) {
|
|
115
|
+
const amount = options.amount || 0.005;
|
|
116
|
+
const freeForHumans = options.freeForHumans ?? false;
|
|
117
|
+
|
|
118
|
+
return async (request, next) => {
|
|
119
|
+
const agent = isAgent(request);
|
|
120
|
+
if (freeForHumans && !agent) return next();
|
|
121
|
+
|
|
122
|
+
const auth = request.headers.get('authorization') || '';
|
|
123
|
+
const token = auth.replace(/^Bearer\s+/i, '');
|
|
124
|
+
if (token && await verifyToken(token, apiKey)) return next();
|
|
125
|
+
|
|
126
|
+
const paymentInfo = build402(request, apiKey, amount);
|
|
127
|
+
return new Response(JSON.stringify(paymentInfo), {
|
|
128
|
+
status: 402,
|
|
129
|
+
headers: { 'Content-Type': 'application/json', 'X-402-Version': '1' },
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { isAgent };
|
package/tollbooth.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentToll SDK
|
|
3
|
+
* One-line integration for AI agent micropayments
|
|
4
|
+
*
|
|
5
|
+
* Usage (Express):
|
|
6
|
+
* app.use(require('@agenttoll/sdk')('your-api-key'))
|
|
7
|
+
*
|
|
8
|
+
* Usage (Cloudflare Worker):
|
|
9
|
+
* import { tollgate } from '@agenttoll/sdk/edge'
|
|
10
|
+
* export default tollgate('your-api-key', { amount: 0.005 })
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TOLL_API_BASE = process.env.TOLL_API_URL || 'https://toll.agenttoll.io';
|
|
14
|
+
|
|
15
|
+
// Agent detection patterns
|
|
16
|
+
const AGENT_PATTERNS = [
|
|
17
|
+
// Platform-specific
|
|
18
|
+
/agenttoll/i,
|
|
19
|
+
/openclaw/i,
|
|
20
|
+
/clawd/i,
|
|
21
|
+
// Major AI providers
|
|
22
|
+
/claude/i,
|
|
23
|
+
/anthropic/i,
|
|
24
|
+
/openai/i,
|
|
25
|
+
/gpt-4/i,
|
|
26
|
+
/chatgpt/i,
|
|
27
|
+
/gemini/i,
|
|
28
|
+
/google-ai/i,
|
|
29
|
+
// Agent frameworks
|
|
30
|
+
/langchain/i,
|
|
31
|
+
/autogpt/i,
|
|
32
|
+
/agentgpt/i,
|
|
33
|
+
/babyagi/i,
|
|
34
|
+
/crewai/i,
|
|
35
|
+
/superagent/i,
|
|
36
|
+
// Generic patterns
|
|
37
|
+
/ai-agent/i,
|
|
38
|
+
/bot\//i,
|
|
39
|
+
/agent\//i,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect if request is from an AgentToll/AI agent
|
|
44
|
+
*/
|
|
45
|
+
function isAgent(req) {
|
|
46
|
+
const ua = req.headers['user-agent'] || '';
|
|
47
|
+
const agentHeader = req.headers['x-agent-type'] || req.headers['x-agenttoll-id'] || '';
|
|
48
|
+
|
|
49
|
+
// Check User-Agent
|
|
50
|
+
if (AGENT_PATTERNS.some(pattern => pattern.test(ua))) return true;
|
|
51
|
+
|
|
52
|
+
// Check custom agent headers
|
|
53
|
+
if (agentHeader) return true;
|
|
54
|
+
|
|
55
|
+
// Check x402 capability header (agents advertise payment capability)
|
|
56
|
+
if (req.headers['x-402-capable'] === 'true') return true;
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verify payment token
|
|
63
|
+
*/
|
|
64
|
+
async function verifyToken(token, apiKey) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(`${TOLL_API_BASE}/api/verify`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'X-Publisher-Key': apiKey,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ token }),
|
|
73
|
+
});
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
return data.valid === true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build 402 Payment Required response for agents
|
|
83
|
+
* Includes onboarding info for non-x402 capable agents
|
|
84
|
+
*/
|
|
85
|
+
function build402Response(req, options) {
|
|
86
|
+
const { apiKey, amount, resource } = options;
|
|
87
|
+
const requestedUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
|
88
|
+
const isX402Capable = req.headers['x-402-capable'] === 'true';
|
|
89
|
+
|
|
90
|
+
const paymentInfo = {
|
|
91
|
+
status: 402,
|
|
92
|
+
message: 'Payment Required',
|
|
93
|
+
agent_instructions: `To access this resource, pay ${amount} USDC via the payment URL below. After payment, retry with the returned token in Authorization header.`,
|
|
94
|
+
payment: {
|
|
95
|
+
amount: amount,
|
|
96
|
+
currency: 'USDC',
|
|
97
|
+
network: 'solana',
|
|
98
|
+
pay_url: `${TOLL_API_BASE}/pay?publisher=${apiKey}&amount=${amount}&resource=${encodeURIComponent(requestedUrl)}`,
|
|
99
|
+
api_endpoint: `${TOLL_API_BASE}/api/pay`,
|
|
100
|
+
},
|
|
101
|
+
x402: {
|
|
102
|
+
version: 1,
|
|
103
|
+
amount: amount,
|
|
104
|
+
currency: 'USDC',
|
|
105
|
+
receiver: options.walletAddress || 'pending',
|
|
106
|
+
network: 'solana-mainnet',
|
|
107
|
+
description: `Access to ${resource || requestedUrl}`,
|
|
108
|
+
},
|
|
109
|
+
retry: {
|
|
110
|
+
method: req.method,
|
|
111
|
+
url: requestedUrl,
|
|
112
|
+
headers: {
|
|
113
|
+
'Authorization': 'Bearer <token_from_payment>',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Add onboarding info for non-x402 capable agents
|
|
119
|
+
if (!isX402Capable) {
|
|
120
|
+
paymentInfo.x402_onboarding = {
|
|
121
|
+
message: "Your agent doesn't appear to have x402 payment capability configured. Here's how to enable it:",
|
|
122
|
+
what_is_x402: "x402 is a protocol that allows AI agents to make micropayments to access premium content and APIs autonomously.",
|
|
123
|
+
setup_steps: [
|
|
124
|
+
"1. Add the 'pay_toll' tool to your agent's tool configuration",
|
|
125
|
+
"2. Configure a Solana wallet with USDC for payments",
|
|
126
|
+
"3. Set budget limits for autonomous spending",
|
|
127
|
+
"4. Add 'x-402-capable: true' header to advertise capability"
|
|
128
|
+
],
|
|
129
|
+
tool_schema: {
|
|
130
|
+
name: "pay_toll",
|
|
131
|
+
description: "Pay a micropayment toll to access a paywalled resource. Use when you encounter HTTP 402 Payment Required responses.",
|
|
132
|
+
parameters: {
|
|
133
|
+
type: "object",
|
|
134
|
+
required: ["pay_url", "amount"],
|
|
135
|
+
properties: {
|
|
136
|
+
pay_url: { type: "string", description: "The payment URL from the 402 response" },
|
|
137
|
+
amount: { type: "number", description: "Payment amount in USDC" },
|
|
138
|
+
max_amount: { type: "number", description: "Maximum you're willing to pay (safety limit)" }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
example_tool_call: {
|
|
143
|
+
tool: "pay_toll",
|
|
144
|
+
arguments: {
|
|
145
|
+
pay_url: paymentInfo.payment.pay_url,
|
|
146
|
+
amount: amount,
|
|
147
|
+
max_amount: 0.05
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
documentation: `${TOLL_API_BASE}/docs#agent-setup`,
|
|
151
|
+
sdk_install: "npm install @agenttoll/sdk",
|
|
152
|
+
sample_integration: `${TOLL_API_BASE}/docs#pay-toll-tool`,
|
|
153
|
+
operator_dashboard: `${TOLL_API_BASE}/dashboard`,
|
|
154
|
+
support: "support@agenttoll.io"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return paymentInfo;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Express/Connect middleware factory
|
|
163
|
+
*
|
|
164
|
+
* @param {string} apiKey - Your publisher API key from toll.agenttoll.io
|
|
165
|
+
* @param {object} options - Configuration options
|
|
166
|
+
* @returns {function} Express middleware
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* // One line integration
|
|
170
|
+
* app.use(tollbooth('pk_live_xxx'));
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* // With options
|
|
174
|
+
* app.use(tollbooth('pk_live_xxx', {
|
|
175
|
+
* amount: 0.01,
|
|
176
|
+
* paths: ['/api/premium/*'],
|
|
177
|
+
* freeForHumans: true
|
|
178
|
+
* }));
|
|
179
|
+
*/
|
|
180
|
+
function tollbooth(apiKey, options = {}) {
|
|
181
|
+
const config = {
|
|
182
|
+
amount: options.amount || 0.005,
|
|
183
|
+
paths: options.paths || ['*'],
|
|
184
|
+
freeForHumans: options.freeForHumans ?? false,
|
|
185
|
+
walletAddress: options.walletAddress || null,
|
|
186
|
+
onPayment: options.onPayment || null,
|
|
187
|
+
bypassHeader: options.bypassHeader || null,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return async function tollboothMiddleware(req, res, next) {
|
|
191
|
+
// Check bypass header (for internal services)
|
|
192
|
+
if (config.bypassHeader && req.headers[config.bypassHeader.toLowerCase()]) {
|
|
193
|
+
return next();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if path should be tolled
|
|
197
|
+
const shouldToll = config.paths.some(pattern => {
|
|
198
|
+
if (pattern === '*') return true;
|
|
199
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
200
|
+
return regex.test(req.path);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!shouldToll) return next();
|
|
204
|
+
|
|
205
|
+
// Detect if agent
|
|
206
|
+
const agentRequest = isAgent(req);
|
|
207
|
+
|
|
208
|
+
// If free for humans and not an agent, pass through
|
|
209
|
+
if (config.freeForHumans && !agentRequest) {
|
|
210
|
+
return next();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for existing payment token
|
|
214
|
+
const authHeader = req.headers['authorization'] || '';
|
|
215
|
+
const token = authHeader.replace(/^Bearer\s+/i, '');
|
|
216
|
+
|
|
217
|
+
if (token) {
|
|
218
|
+
const valid = await verifyToken(token, apiKey);
|
|
219
|
+
if (valid) {
|
|
220
|
+
// Track successful access
|
|
221
|
+
req.tollPaid = true;
|
|
222
|
+
req.tollAgent = agentRequest;
|
|
223
|
+
return next();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// No valid token - return 402 Payment Required
|
|
228
|
+
const paymentInfo = build402Response(req, {
|
|
229
|
+
apiKey,
|
|
230
|
+
amount: config.amount,
|
|
231
|
+
resource: req.path,
|
|
232
|
+
walletAddress: config.walletAddress,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Check if agent advertises x402 capability
|
|
236
|
+
const isX402Capable = req.headers['x-402-capable'] === 'true';
|
|
237
|
+
|
|
238
|
+
// Set x402 headers for agent parsing
|
|
239
|
+
res.setHeader('X-402-Version', '1');
|
|
240
|
+
res.setHeader('X-402-Amount', config.amount.toString());
|
|
241
|
+
res.setHeader('X-402-Currency', 'USDC');
|
|
242
|
+
res.setHeader('X-402-Pay-URL', paymentInfo.payment.pay_url);
|
|
243
|
+
res.setHeader('X-402-Network', 'solana');
|
|
244
|
+
res.setHeader('Content-Type', 'application/json');
|
|
245
|
+
|
|
246
|
+
// Add onboarding hint for non-capable agents
|
|
247
|
+
if (!isX402Capable) {
|
|
248
|
+
res.setHeader('X-402-Onboarding', 'true');
|
|
249
|
+
res.setHeader('X-402-Setup-URL', `${TOLL_API_BASE}/docs#agent-setup`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return res.status(402).json(paymentInfo);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Protect specific routes (alternative API)
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* app.get('/premium', tollbooth.protect('pk_xxx', { amount: 0.01 }), handler)
|
|
261
|
+
*/
|
|
262
|
+
tollbooth.protect = function(apiKey, options = {}) {
|
|
263
|
+
return tollbooth(apiKey, { ...options, paths: ['*'] });
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Agent-only tollbooth (humans pass free)
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* app.use(tollbooth.agentsOnly('pk_xxx'))
|
|
271
|
+
*/
|
|
272
|
+
tollbooth.agentsOnly = function(apiKey, options = {}) {
|
|
273
|
+
return tollbooth(apiKey, { ...options, freeForHumans: true });
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if request has valid toll payment
|
|
278
|
+
*/
|
|
279
|
+
tollbooth.hasPaid = function(req) {
|
|
280
|
+
return req.tollPaid === true;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if request is from an agent
|
|
285
|
+
*/
|
|
286
|
+
tollbooth.isAgent = isAgent;
|
|
287
|
+
|
|
288
|
+
// Export for different module systems
|
|
289
|
+
module.exports = tollbooth;
|
|
290
|
+
module.exports.default = tollbooth;
|
|
291
|
+
module.exports.tollbooth = tollbooth;
|
|
292
|
+
module.exports.isAgent = isAgent;
|