@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 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.