@xcheeze/x402 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 +49 -0
- package/dist/config.js +20 -0
- package/dist/index.js +113 -0
- package/dist/wallet.js +102 -0
- package/dist/x402.js +97 -0
- package/package.json +43 -0
- package/skill.md +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @xcheeze/x402
|
|
2
|
+
|
|
3
|
+
CLI for AI agents to pay a human on Cheeze over the open
|
|
4
|
+
[x402](https://developers.circle.com/gateway/nanopayments/concepts/x402)
|
|
5
|
+
protocol (Circle Gateway nanopayments — EIP-3009 `TransferWithAuthorization`
|
|
6
|
+
against the `GatewayWalletBatched` domain). The agent funds its own
|
|
7
|
+
Arc-**testnet** wallet and delivers a paid message into the person's
|
|
8
|
+
Cheeze inbox. **No Cheeze account or API key.** Mainnet is not live.
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @xcheeze/x402 wallet # create the agent wallet, print address + faucet
|
|
14
|
+
# → at https://faucet.circle.com/ pick "Arc Testnet", paste the address;
|
|
15
|
+
# the faucet drips 20 testnet USDC per request
|
|
16
|
+
npx @xcheeze/x402 fund # deposit on-chain USDC → Circle Gateway pocket
|
|
17
|
+
npx @xcheeze/x402 balance # confirm pocket available > 0
|
|
18
|
+
npx @xcheeze/x402 send @hudson --subject "Hello" --message "…"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| Command | Purpose |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `wallet` | Create / show the local agent wallet (`~/.cheeze/x402.json`, or `CHEEZE_X402_PRIVATE_KEY`). |
|
|
26
|
+
| `balance` | On-chain USDC + Gateway pocket (available / pending). |
|
|
27
|
+
| `fund [--amount N]` | Deposit on-chain USDC into the Gateway pocket (leaves a gas buffer; Arc gas is USDC). |
|
|
28
|
+
| `quote @handle [read\|deliver]` | Show the gross price — no payment. |
|
|
29
|
+
| `read @handle` | Pay to read the public summary. |
|
|
30
|
+
| `send @handle --subject "..." --message "..."` | Pay to deliver a message to the inbox. |
|
|
31
|
+
|
|
32
|
+
## Constraints
|
|
33
|
+
|
|
34
|
+
- Subject ≤ 60 chars, message ≤ 420 chars (mirrors the seller; the
|
|
35
|
+
**server is authoritative** and rejects over-length / filtered
|
|
36
|
+
content **before** charging — you are never billed for a rejected
|
|
37
|
+
message).
|
|
38
|
+
- You pay exactly the price the `402` returns — nothing else.
|
|
39
|
+
- Testnet keys only.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
`send` → `GET /x402/<handle>/deliver` → `402` with the price →
|
|
44
|
+
EIP-3009 authorization signed locally by your key (gasless) → retry
|
|
45
|
+
with the `payment-signature` header + the message body → the message
|
|
46
|
+
is delivered to the person's Cheeze inbox. You pay exactly the price
|
|
47
|
+
returned in the `402`.
|
|
48
|
+
|
|
49
|
+
Build: `npm i && npm run build` (outputs `dist/`, bin `xcheeze-x402`).
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineChain } from 'viem';
|
|
2
|
+
export const FACILITATOR_BASE = 'https://facilitator.cheeze.com';
|
|
3
|
+
export const ARC_CHAIN_ID = 5042002;
|
|
4
|
+
export const ARC_RPC = 'https://rpc.testnet.arc.network';
|
|
5
|
+
export const ARC_EXPLORER = 'https://testnet.arcscan.app';
|
|
6
|
+
export const USDC_ADDRESS = '0x3600000000000000000000000000000000000000';
|
|
7
|
+
export const GATEWAY_WALLET = '0x0077777d7EBA4688BDeF3E311b846F25870A19B9';
|
|
8
|
+
export const ARC_GATEWAY_DOMAIN = 26;
|
|
9
|
+
export const FAUCET_URL = 'https://faucet.circle.com/';
|
|
10
|
+
export const FAUCET_AMOUNT_USDC = 20;
|
|
11
|
+
export const MAX_SUBJECT_CHARS = 60;
|
|
12
|
+
export const MAX_MESSAGE_CHARS = 420;
|
|
13
|
+
export const arcTestnet = defineChain({
|
|
14
|
+
id: ARC_CHAIN_ID,
|
|
15
|
+
name: 'Arc Testnet',
|
|
16
|
+
nativeCurrency: { name: 'USDC', symbol: 'USDC', decimals: 18 },
|
|
17
|
+
rpcUrls: { default: { http: [ARC_RPC] } },
|
|
18
|
+
blockExplorers: { default: { name: 'Arcscan', url: ARC_EXPLORER } },
|
|
19
|
+
testnet: true,
|
|
20
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createKey, loadKey, account, onchainUsdc, gatewayPocket, depositToPocket, usdc6, } from './wallet.js';
|
|
3
|
+
import { fetchChallenge, priceUsd, pay } from './x402.js';
|
|
4
|
+
import { FAUCET_URL, FAUCET_AMOUNT_USDC, ARC_EXPLORER, MAX_SUBJECT_CHARS, MAX_MESSAGE_CHARS, } from './config.js';
|
|
5
|
+
function flag(argv, name) {
|
|
6
|
+
const i = argv.indexOf(`--${name}`);
|
|
7
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
8
|
+
}
|
|
9
|
+
function fail(msg) {
|
|
10
|
+
console.error(`✗ ${msg}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const USAGE = `@xcheeze/x402 — message a human on Cheeze over x402 (Arc testnet)
|
|
14
|
+
|
|
15
|
+
xcheeze-x402 wallet create / show your agent wallet
|
|
16
|
+
xcheeze-x402 balance on-chain USDC + Gateway pocket
|
|
17
|
+
xcheeze-x402 fund [--amount <usdc>] deposit on-chain USDC into the pocket
|
|
18
|
+
xcheeze-x402 quote @handle [read|deliver] show the price — no payment
|
|
19
|
+
xcheeze-x402 read @handle pay to read @handle's public summary
|
|
20
|
+
xcheeze-x402 send @handle --subject "<s>" --message "<m>"
|
|
21
|
+
|
|
22
|
+
Setup: 1) wallet 2) send testnet USDC to the address via the faucet
|
|
23
|
+
3) fund 4) send. No Cheeze account or API key needed.`;
|
|
24
|
+
async function main() {
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
const cmd = argv[0];
|
|
27
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
28
|
+
console.log(USAGE);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (cmd === 'wallet') {
|
|
32
|
+
let addr;
|
|
33
|
+
if (loadKey()) {
|
|
34
|
+
addr = account().address;
|
|
35
|
+
console.log(`Wallet (existing): ${addr}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
addr = createKey().address;
|
|
39
|
+
console.log(`Wallet created: ${addr}`);
|
|
40
|
+
console.log('Saved to ~/.cheeze/x402.json (chmod 600). Back it up.');
|
|
41
|
+
}
|
|
42
|
+
console.log(`\nNext:\n` +
|
|
43
|
+
` 1. Get testnet USDC: open ${FAUCET_URL}\n` +
|
|
44
|
+
` pick "Arc Testnet", paste ${addr}\n` +
|
|
45
|
+
` (the faucet drips ${FAUCET_AMOUNT_USDC} USDC per request)\n` +
|
|
46
|
+
` 2. xcheeze-x402 fund # deposit it into your Gateway pocket\n` +
|
|
47
|
+
` 3. xcheeze-x402 balance # confirm pocket available > 0\n` +
|
|
48
|
+
` 4. xcheeze-x402 send @handle --subject "..." --message "..."`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (cmd === 'balance') {
|
|
52
|
+
const a = account();
|
|
53
|
+
const [oc, pk] = await Promise.all([onchainUsdc(a.address), gatewayPocket(a.address)]);
|
|
54
|
+
console.log(`Address: ${a.address}`);
|
|
55
|
+
console.log(`On-chain USDC: ${usdc6(oc)} (deposit this into the pocket)`);
|
|
56
|
+
console.log(`Gateway pocket: ${pk.available} available · ${pk.pending} pending (x402 spends from here)`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (cmd === 'fund') {
|
|
60
|
+
const a = account();
|
|
61
|
+
const oc = await onchainUsdc(a.address);
|
|
62
|
+
if (oc <= 0n) {
|
|
63
|
+
fail(`No on-chain USDC at ${a.address}. Get testnet USDC from the faucet first: ${FAUCET_URL}`);
|
|
64
|
+
}
|
|
65
|
+
const amt = flag(argv, 'amount');
|
|
66
|
+
const amountUnits = amt ? BigInt(Math.round(Number(amt) * 1_000_000)) : undefined;
|
|
67
|
+
console.log(`Depositing into the Gateway pocket (leaving a gas buffer)…`);
|
|
68
|
+
const r = await depositToPocket(1000000n, amountUnits);
|
|
69
|
+
console.log(`✓ approve ${ARC_EXPLORER}/tx/${r.approveTx}`);
|
|
70
|
+
console.log(`✓ deposit ${ARC_EXPLORER}/tx/${r.depositTx}`);
|
|
71
|
+
console.log(`Pocket available: ${r.pocket} USDC`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (cmd === 'quote') {
|
|
75
|
+
const handle = argv[1];
|
|
76
|
+
const act = argv[2] || 'deliver';
|
|
77
|
+
if (!handle)
|
|
78
|
+
fail('Usage: xcheeze-x402 quote @handle [read|deliver]');
|
|
79
|
+
const { accept } = await fetchChallenge(handle, act);
|
|
80
|
+
console.log(`${handle} · ${act}: ${priceUsd(accept)} USDC (the price you pay)`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (cmd === 'read') {
|
|
84
|
+
const handle = argv[1];
|
|
85
|
+
if (!handle)
|
|
86
|
+
fail('Usage: xcheeze-x402 read @handle');
|
|
87
|
+
const out = await pay(handle, 'read');
|
|
88
|
+
console.log(JSON.stringify(out, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (cmd === 'send') {
|
|
92
|
+
const handle = argv[1];
|
|
93
|
+
const subject = flag(argv, 'subject') ?? '';
|
|
94
|
+
const message = flag(argv, 'message') ?? '';
|
|
95
|
+
if (!handle)
|
|
96
|
+
fail('Usage: xcheeze-x402 send @handle --subject "..." --message "..."');
|
|
97
|
+
if (!message.trim())
|
|
98
|
+
fail('A non-empty --message is required.');
|
|
99
|
+
if (subject.length > MAX_SUBJECT_CHARS) {
|
|
100
|
+
fail(`Subject too long: ${subject.length}/${MAX_SUBJECT_CHARS} chars.`);
|
|
101
|
+
}
|
|
102
|
+
if (message.length > MAX_MESSAGE_CHARS) {
|
|
103
|
+
fail(`Message too long: ${message.length}/${MAX_MESSAGE_CHARS} chars.`);
|
|
104
|
+
}
|
|
105
|
+
const { accept } = await fetchChallenge(handle, 'deliver');
|
|
106
|
+
console.error(`Paying ${priceUsd(accept)} USDC to message ${handle}…`);
|
|
107
|
+
const out = await pay(handle, 'deliver', { message, subject: subject || undefined });
|
|
108
|
+
console.log(JSON.stringify(out, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
fail(`Unknown command "${cmd}".\n\n${USAGE}`);
|
|
112
|
+
}
|
|
113
|
+
main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createPublicClient, createWalletClient, http, parseAbi, formatUnits, } from 'viem';
|
|
5
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
6
|
+
import { arcTestnet, ARC_RPC, USDC_ADDRESS, GATEWAY_WALLET, ARC_GATEWAY_DOMAIN, } from './config.js';
|
|
7
|
+
const KEYSTORE_DIR = join(homedir(), '.cheeze');
|
|
8
|
+
const KEYSTORE = join(KEYSTORE_DIR, 'x402.json');
|
|
9
|
+
const USDC_ABI = parseAbi([
|
|
10
|
+
'function balanceOf(address) view returns (uint256)',
|
|
11
|
+
'function approve(address spender, uint256 value) returns (bool)',
|
|
12
|
+
]);
|
|
13
|
+
const GATEWAY_ABI = parseAbi([
|
|
14
|
+
'function deposit(address token, uint256 value)',
|
|
15
|
+
]);
|
|
16
|
+
function isHexKey(s) {
|
|
17
|
+
return /^0x[0-9a-fA-F]{64}$/.test(s);
|
|
18
|
+
}
|
|
19
|
+
export function loadKey() {
|
|
20
|
+
const env = process.env.CHEEZE_X402_PRIVATE_KEY?.trim();
|
|
21
|
+
if (env && isHexKey(env))
|
|
22
|
+
return env;
|
|
23
|
+
if (existsSync(KEYSTORE)) {
|
|
24
|
+
try {
|
|
25
|
+
const k = JSON.parse(readFileSync(KEYSTORE, 'utf8'))
|
|
26
|
+
.privateKey?.trim();
|
|
27
|
+
if (k && isHexKey(k))
|
|
28
|
+
return k;
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function createKey() {
|
|
35
|
+
if (loadKey())
|
|
36
|
+
throw new Error('A wallet already exists (env or keystore). Refusing to overwrite.');
|
|
37
|
+
const pk = generatePrivateKey();
|
|
38
|
+
mkdirSync(KEYSTORE_DIR, { recursive: true });
|
|
39
|
+
writeFileSync(KEYSTORE, JSON.stringify({ privateKey: pk }, null, 2));
|
|
40
|
+
chmodSync(KEYSTORE, 0o600);
|
|
41
|
+
return { address: privateKeyToAccount(pk).address };
|
|
42
|
+
}
|
|
43
|
+
export function account() {
|
|
44
|
+
const pk = loadKey();
|
|
45
|
+
if (!pk)
|
|
46
|
+
throw new Error('No wallet. Run `xcheeze-x402 wallet` first.');
|
|
47
|
+
return privateKeyToAccount(pk);
|
|
48
|
+
}
|
|
49
|
+
const publicClient = () => createPublicClient({ chain: arcTestnet, transport: http(ARC_RPC) });
|
|
50
|
+
export async function onchainUsdc(address) {
|
|
51
|
+
return publicClient().readContract({
|
|
52
|
+
address: USDC_ADDRESS, abi: USDC_ABI, functionName: 'balanceOf', args: [address],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export async function gatewayPocket(address) {
|
|
56
|
+
const r = await fetch('https://gateway-api-testnet.circle.com/v1/balances', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
token: 'USDC',
|
|
61
|
+
sources: [{ domain: ARC_GATEWAY_DOMAIN, depositor: address }],
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
if (!r.ok)
|
|
65
|
+
return { available: 0, pending: 0 };
|
|
66
|
+
const b = (await r.json())?.balances?.[0];
|
|
67
|
+
return {
|
|
68
|
+
available: Number.parseFloat(String(b?.balance ?? '0')) || 0,
|
|
69
|
+
pending: Number.parseFloat(String(b?.pendingBatch ?? '0')) || 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function usdc6(units) {
|
|
73
|
+
return formatUnits(units, 6);
|
|
74
|
+
}
|
|
75
|
+
export async function depositToPocket(bufferUnits = 1000000n, amountUnits) {
|
|
76
|
+
const acct = account();
|
|
77
|
+
const wallet = createWalletClient({ account: acct, chain: arcTestnet, transport: http(ARC_RPC) });
|
|
78
|
+
const pub = publicClient();
|
|
79
|
+
const bal = await onchainUsdc(acct.address);
|
|
80
|
+
const amount = amountUnits ?? (bal > bufferUnits ? bal - bufferUnits : 0n);
|
|
81
|
+
if (amount <= 0n) {
|
|
82
|
+
throw new Error(`Not enough on-chain USDC to deposit (have ${usdc6(bal)}, need > ${usdc6(bufferUnits)} for gas buffer). Fund the wallet at the faucet first.`);
|
|
83
|
+
}
|
|
84
|
+
const approveTx = await wallet.writeContract({
|
|
85
|
+
address: USDC_ADDRESS, abi: USDC_ABI, functionName: 'approve',
|
|
86
|
+
args: [GATEWAY_WALLET, amount],
|
|
87
|
+
});
|
|
88
|
+
await pub.waitForTransactionReceipt({ hash: approveTx });
|
|
89
|
+
const depositTx = await wallet.writeContract({
|
|
90
|
+
address: GATEWAY_WALLET, abi: GATEWAY_ABI, functionName: 'deposit',
|
|
91
|
+
args: [USDC_ADDRESS, amount],
|
|
92
|
+
});
|
|
93
|
+
await pub.waitForTransactionReceipt({ hash: depositTx });
|
|
94
|
+
let pocket = 0;
|
|
95
|
+
for (let i = 0; i < 15; i++) {
|
|
96
|
+
pocket = (await gatewayPocket(acct.address)).available;
|
|
97
|
+
if (pocket > 0)
|
|
98
|
+
break;
|
|
99
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
100
|
+
}
|
|
101
|
+
return { approveTx, depositTx, pocket };
|
|
102
|
+
}
|
package/dist/x402.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { account } from './wallet.js';
|
|
2
|
+
import { FACILITATOR_BASE, ARC_CHAIN_ID, GATEWAY_WALLET, } from './config.js';
|
|
3
|
+
const b64 = (s) => Buffer.from(s, 'utf8').toString('base64');
|
|
4
|
+
const unb64 = (s) => Buffer.from(s, 'base64').toString('utf8');
|
|
5
|
+
function url(handle, action) {
|
|
6
|
+
const h = handle.replace(/^@/, '');
|
|
7
|
+
return `${FACILITATOR_BASE}/x402/${encodeURIComponent(h)}/${action}`;
|
|
8
|
+
}
|
|
9
|
+
export async function fetchChallenge(handle, action) {
|
|
10
|
+
const res = await fetch(url(handle, action));
|
|
11
|
+
const hdr = res.headers.get('payment-required') || res.headers.get('PAYMENT-REQUIRED');
|
|
12
|
+
if (!hdr) {
|
|
13
|
+
const body = await res.text().catch(() => '');
|
|
14
|
+
throw new Error(`No PAYMENT-REQUIRED from ${handle} (HTTP ${res.status}). ${body.slice(0, 200)}`);
|
|
15
|
+
}
|
|
16
|
+
const challenge = JSON.parse(unb64(hdr));
|
|
17
|
+
const accept = challenge?.accepts?.[0];
|
|
18
|
+
if (!accept)
|
|
19
|
+
throw new Error('Malformed 402 challenge (no accepts).');
|
|
20
|
+
return { accept, challenge };
|
|
21
|
+
}
|
|
22
|
+
export function priceUsd(accept) {
|
|
23
|
+
return (Number(accept.amount) || 0) / 1_000_000;
|
|
24
|
+
}
|
|
25
|
+
function randomNonce() {
|
|
26
|
+
const b = crypto.getRandomValues(new Uint8Array(32));
|
|
27
|
+
return ('0x' + [...b].map((x) => x.toString(16).padStart(2, '0')).join(''));
|
|
28
|
+
}
|
|
29
|
+
export async function pay(handle, action, body) {
|
|
30
|
+
const acct = account();
|
|
31
|
+
const { accept: a, challenge } = await fetchChallenge(handle, action);
|
|
32
|
+
const nonce = randomNonce();
|
|
33
|
+
const value = BigInt(a.amount);
|
|
34
|
+
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 605_000);
|
|
35
|
+
const signature = await acct.signTypedData({
|
|
36
|
+
domain: {
|
|
37
|
+
name: a.extra?.name ?? 'GatewayWalletBatched',
|
|
38
|
+
version: a.extra?.version ?? '1',
|
|
39
|
+
chainId: ARC_CHAIN_ID,
|
|
40
|
+
verifyingContract: a.extra?.verifyingContract ?? GATEWAY_WALLET,
|
|
41
|
+
},
|
|
42
|
+
types: {
|
|
43
|
+
TransferWithAuthorization: [
|
|
44
|
+
{ name: 'from', type: 'address' },
|
|
45
|
+
{ name: 'to', type: 'address' },
|
|
46
|
+
{ name: 'value', type: 'uint256' },
|
|
47
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
48
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
49
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
primaryType: 'TransferWithAuthorization',
|
|
53
|
+
message: { from: acct.address, to: a.payTo, value, validAfter: 0n, validBefore, nonce },
|
|
54
|
+
});
|
|
55
|
+
const authorization = {
|
|
56
|
+
from: acct.address,
|
|
57
|
+
to: a.payTo,
|
|
58
|
+
value: String(a.amount),
|
|
59
|
+
validAfter: '0',
|
|
60
|
+
validBefore: String(validBefore),
|
|
61
|
+
nonce,
|
|
62
|
+
};
|
|
63
|
+
const paymentPayload = {
|
|
64
|
+
x402Version: 2,
|
|
65
|
+
scheme: a.scheme,
|
|
66
|
+
network: a.network,
|
|
67
|
+
payload: { authorization, signature },
|
|
68
|
+
resource: challenge?.resource ?? {},
|
|
69
|
+
accepted: a,
|
|
70
|
+
};
|
|
71
|
+
const sigHeader = b64(JSON.stringify(paymentPayload));
|
|
72
|
+
const res = action === 'deliver'
|
|
73
|
+
? await fetch(url(handle, action), {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'payment-signature': sigHeader, 'content-type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
message: String(body?.message ?? ''),
|
|
78
|
+
...(body?.subject ? { subject: String(body.subject) } : {}),
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
81
|
+
: await fetch(url(handle, action), { headers: { 'payment-signature': sigHeader } });
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
let data;
|
|
84
|
+
try {
|
|
85
|
+
data = JSON.parse(text);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
data = text;
|
|
89
|
+
}
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const detail = typeof data === 'object' && data
|
|
92
|
+
? JSON.stringify(data)
|
|
93
|
+
: String(data).slice(0, 300);
|
|
94
|
+
throw new Error(`Rejected (HTTP ${res.status}) — not charged. ${detail}`);
|
|
95
|
+
}
|
|
96
|
+
return data;
|
|
97
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xcheeze/x402",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pay a human on Cheeze over x402. An AI agent funds an Arc-testnet wallet and sends a paid message that lands in the human's Cheeze inbox. No Cheeze account or API key.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xcheeze-x402": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"skill.md",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"viem": "^2.21.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsx": "^4.19.0",
|
|
30
|
+
"typescript": "^5.5.0",
|
|
31
|
+
"@types/node": "^20.14.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"x402",
|
|
35
|
+
"cheeze",
|
|
36
|
+
"agent",
|
|
37
|
+
"usdc",
|
|
38
|
+
"arc",
|
|
39
|
+
"circle-gateway",
|
|
40
|
+
"nanopayments"
|
|
41
|
+
],
|
|
42
|
+
"license": "UNLICENSED"
|
|
43
|
+
}
|
package/skill.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Skill: message a human on Cheeze (x402)
|
|
2
|
+
|
|
3
|
+
You can pay to deliver a short message into a real person's Cheeze
|
|
4
|
+
inbox over the open x402 protocol. The person sets their price; the
|
|
5
|
+
`402` returns the price you pay — that exact amount, nothing else. No
|
|
6
|
+
Cheeze account or API key — you use your own wallet. Arc **testnet**
|
|
7
|
+
only (mainnet is not live).
|
|
8
|
+
|
|
9
|
+
## When to use this
|
|
10
|
+
|
|
11
|
+
The user asked you to reach / notify / pitch a specific person who has
|
|
12
|
+
a Cheeze handle (e.g. `@hudson`). One message = one micro-payment.
|
|
13
|
+
|
|
14
|
+
## One-time setup (do this once, then reuse the wallet)
|
|
15
|
+
|
|
16
|
+
**Step 1 — create your wallet.**
|
|
17
|
+
```bash
|
|
18
|
+
npx @xcheeze/x402 wallet
|
|
19
|
+
```
|
|
20
|
+
This generates a fresh Arc-testnet key **on this machine only** and
|
|
21
|
+
prints your wallet **address**. The private key is written to
|
|
22
|
+
`~/.cheeze/x402.json` (chmod 600), or you may instead set it in the
|
|
23
|
+
`CHEEZE_X402_PRIVATE_KEY` env var. It is YOUR key — it is never sent
|
|
24
|
+
to Cheeze and Cheeze never holds funds for you.
|
|
25
|
+
|
|
26
|
+
**Step 2 — get testnet USDC from the faucet.**
|
|
27
|
+
Open **https://faucet.circle.com/** , choose **Arc Testnet**, paste
|
|
28
|
+
the address from Step 1, and request. The faucet drips **20 USDC**
|
|
29
|
+
(testnet) per request. Wait until it arrives (a minute or so).
|
|
30
|
+
|
|
31
|
+
**Step 3 — move it into your Gateway pocket.**
|
|
32
|
+
```bash
|
|
33
|
+
npx @xcheeze/x402 fund
|
|
34
|
+
```
|
|
35
|
+
x402 does **not** spend from your raw wallet — it spends from your
|
|
36
|
+
**Circle Gateway pocket** (a one-time on-chain deposit; payments
|
|
37
|
+
afterwards are gasless). `fund` runs the `approve` + `deposit` for you
|
|
38
|
+
and leaves a small USDC buffer for gas (Arc gas is paid in USDC).
|
|
39
|
+
|
|
40
|
+
**Step 4 — confirm.**
|
|
41
|
+
```bash
|
|
42
|
+
npx @xcheeze/x402 balance
|
|
43
|
+
```
|
|
44
|
+
Proceed once `Gateway pocket available` is greater than 0. You only
|
|
45
|
+
repeat Steps 2–3 when the pocket runs low.
|
|
46
|
+
|
|
47
|
+
## Discover the price (no payment)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @xcheeze/x402 quote @handle deliver
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Returns the USDC price you will pay. The price is also returned
|
|
54
|
+
automatically in the `402` challenge when you send — you never need
|
|
55
|
+
an account to learn it.
|
|
56
|
+
|
|
57
|
+
## Send a paid message
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @xcheeze/x402 send @handle --subject "<subject>" --message "<message>"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Flow (handled for you): request → `402` price → sign an EIP-3009
|
|
64
|
+
authorization with your wallet (gasless, off-chain) → retry with the
|
|
65
|
+
signed payment → the message lands in the human's Cheeze inbox.
|
|
66
|
+
|
|
67
|
+
## Rules you must respect
|
|
68
|
+
|
|
69
|
+
- **Length**: subject ≤ 60 chars, message ≤ 420 chars. Over-length
|
|
70
|
+
is rejected.
|
|
71
|
+
- **Content**: the recipient's server filters and **rejects**
|
|
72
|
+
spam/abusive/policy-violating or manipulative content. Rejections
|
|
73
|
+
happen **before** any charge — you are not billed for a rejected
|
|
74
|
+
message. Send genuine, relevant, well-formed messages only.
|
|
75
|
+
- One payment delivers one message. Don't retry a rejected message
|
|
76
|
+
unchanged; fix it or stop.
|
|
77
|
+
- Funds are testnet only. Do not use real-value keys.
|
|
78
|
+
|
|
79
|
+
## Interpreting results
|
|
80
|
+
|
|
81
|
+
- Success: JSON receipt with `settlement: "settled"` — delivered, you
|
|
82
|
+
were charged the quoted price.
|
|
83
|
+
- `Rejected (HTTP 4xx) — not charged`: length/content/validation
|
|
84
|
+
failure. You were **not** billed. Do not blindly retry.
|