repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * x402 Payment Protocol |
| #3 | * |
| #4 | * Enables the automaton to make USDC micropayments via HTTP 402. |
| #5 | * Adapted from runtime-mcp/src/x402/index.ts |
| #6 | */ |
| #7 | import { createPublicClient, http, parseUnits, } from "viem"; |
| #8 | import { base, baseSepolia } from "viem/chains"; |
| #9 | // USDC contract addresses |
| #10 | const USDC_ADDRESSES = { |
| #11 | "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base mainnet |
| #12 | "eip155:84532": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia |
| #13 | }; |
| #14 | const CHAINS = { |
| #15 | "eip155:8453": base, |
| #16 | "eip155:84532": baseSepolia, |
| #17 | }; |
| #18 | const BALANCE_OF_ABI = [ |
| #19 | { |
| #20 | inputs: [{ name: "account", type: "address" }], |
| #21 | name: "balanceOf", |
| #22 | outputs: [{ name: "", type: "uint256" }], |
| #23 | stateMutability: "view", |
| #24 | type: "function", |
| #25 | }, |
| #26 | ]; |
| #27 | function safeJsonParse(value) { |
| #28 | try { |
| #29 | return JSON.parse(value); |
| #30 | } |
| #31 | catch { |
| #32 | return null; |
| #33 | } |
| #34 | } |
| #35 | function parsePositiveInt(value) { |
| #36 | if (typeof value === "number" && Number.isFinite(value) && value > 0) { |
| #37 | return Math.floor(value); |
| #38 | } |
| #39 | if (typeof value === "string" && value.trim()) { |
| #40 | const parsed = Number(value); |
| #41 | if (Number.isFinite(parsed) && parsed > 0) { |
| #42 | return Math.floor(parsed); |
| #43 | } |
| #44 | } |
| #45 | return null; |
| #46 | } |
| #47 | function normalizeNetwork(raw) { |
| #48 | if (typeof raw !== "string") |
| #49 | return null; |
| #50 | const normalized = raw.trim().toLowerCase(); |
| #51 | if (normalized === "base") |
| #52 | return "eip155:8453"; |
| #53 | if (normalized === "base-sepolia") |
| #54 | return "eip155:84532"; |
| #55 | if (normalized === "eip155:8453" || normalized === "eip155:84532") { |
| #56 | return normalized; |
| #57 | } |
| #58 | return null; |
| #59 | } |
| #60 | function normalizePaymentRequirement(raw) { |
| #61 | if (typeof raw !== "object" || raw === null) |
| #62 | return null; |
| #63 | const value = raw; |
| #64 | const network = normalizeNetwork(value.network); |
| #65 | if (!network) |
| #66 | return null; |
| #67 | const scheme = typeof value.scheme === "string" ? value.scheme : null; |
| #68 | const maxAmountRequired = typeof value.maxAmountRequired === "string" |
| #69 | ? value.maxAmountRequired |
| #70 | : typeof value.maxAmountRequired === "number" && |
| #71 | Number.isFinite(value.maxAmountRequired) |
| #72 | ? String(value.maxAmountRequired) |
| #73 | : null; |
| #74 | const payToAddress = typeof value.payToAddress === "string" |
| #75 | ? value.payToAddress |
| #76 | : typeof value.payTo === "string" |
| #77 | ? value.payTo |
| #78 | : null; |
| #79 | const usdcAddress = typeof value.usdcAddress === "string" |
| #80 | ? value.usdcAddress |
| #81 | : typeof value.asset === "string" |
| #82 | ? value.asset |
| #83 | : USDC_ADDRESSES[network]; |
| #84 | const requiredDeadlineSeconds = parsePositiveInt(value.requiredDeadlineSeconds) ?? |
| #85 | parsePositiveInt(value.maxTimeoutSeconds) ?? |
| #86 | 300; |
| #87 | if (!scheme || !maxAmountRequired || !payToAddress || !usdcAddress) { |
| #88 | return null; |
| #89 | } |
| #90 | return { |
| #91 | scheme, |
| #92 | network, |
| #93 | maxAmountRequired, |
| #94 | payToAddress: payToAddress, |
| #95 | requiredDeadlineSeconds, |
| #96 | usdcAddress: usdcAddress, |
| #97 | }; |
| #98 | } |
| #99 | function normalizePaymentRequired(raw) { |
| #100 | if (typeof raw !== "object" || raw === null) |
| #101 | return null; |
| #102 | const value = raw; |
| #103 | if (!Array.isArray(value.accepts)) |
| #104 | return null; |
| #105 | const accepts = value.accepts |
| #106 | .map(normalizePaymentRequirement) |
| #107 | .filter((v) => v !== null); |
| #108 | if (!accepts.length) |
| #109 | return null; |
| #110 | const x402Version = parsePositiveInt(value.x402Version) ?? 1; |
| #111 | return { x402Version, accepts }; |
| #112 | } |
| #113 | function parseMaxAmountRequired(maxAmountRequired, x402Version) { |
| #114 | const amount = maxAmountRequired.trim(); |
| #115 | if (!/^\d+(\.\d+)?$/.test(amount)) { |
| #116 | throw new Error(`Invalid maxAmountRequired: ${maxAmountRequired}`); |
| #117 | } |
| #118 | if (amount.includes(".")) { |
| #119 | return parseUnits(amount, 6); |
| #120 | } |
| #121 | if (x402Version >= 2 || amount.length > 6) { |
| #122 | return BigInt(amount); |
| #123 | } |
| #124 | return parseUnits(amount, 6); |
| #125 | } |
| #126 | function selectRequirement(parsed) { |
| #127 | const exactSupported = parsed.accepts.find((r) => r.scheme === "exact" && !!CHAINS[r.network]); |
| #128 | if (exactSupported) |
| #129 | return exactSupported; |
| #130 | return parsed.accepts[0]; |
| #131 | } |
| #132 | /** |
| #133 | * Get the USDC balance for the automaton's wallet on a given network. |
| #134 | */ |
| #135 | export async function getUsdcBalance(address, network = "eip155:8453") { |
| #136 | const result = await getUsdcBalanceDetailed(address, network); |
| #137 | return result.balance; |
| #138 | } |
| #139 | /** |
| #140 | * Get the USDC balance and read status details for diagnostics. |
| #141 | */ |
| #142 | export async function getUsdcBalanceDetailed(address, network = "eip155:8453") { |
| #143 | const chain = CHAINS[network]; |
| #144 | const usdcAddress = USDC_ADDRESSES[network]; |
| #145 | if (!chain || !usdcAddress) { |
| #146 | return { |
| #147 | balance: 0, |
| #148 | network, |
| #149 | ok: false, |
| #150 | error: `Unsupported USDC network: ${network}`, |
| #151 | }; |
| #152 | } |
| #153 | try { |
| #154 | const client = createPublicClient({ |
| #155 | chain, |
| #156 | transport: http(), |
| #157 | }); |
| #158 | const balance = await client.readContract({ |
| #159 | address: usdcAddress, |
| #160 | abi: BALANCE_OF_ABI, |
| #161 | functionName: "balanceOf", |
| #162 | args: [address], |
| #163 | }); |
| #164 | // USDC has 6 decimals |
| #165 | return { |
| #166 | balance: Number(balance) / 1_000_000, |
| #167 | network, |
| #168 | ok: true, |
| #169 | }; |
| #170 | } |
| #171 | catch (err) { |
| #172 | return { |
| #173 | balance: 0, |
| #174 | network, |
| #175 | ok: false, |
| #176 | error: err?.message || String(err), |
| #177 | }; |
| #178 | } |
| #179 | } |
| #180 | /** |
| #181 | * Check if a URL requires x402 payment. |
| #182 | */ |
| #183 | export async function checkX402(url) { |
| #184 | try { |
| #185 | const resp = await fetch(url, { method: "GET" }); |
| #186 | if (resp.status !== 402) { |
| #187 | return null; |
| #188 | } |
| #189 | const parsed = await parsePaymentRequired(resp); |
| #190 | return parsed?.requirement ?? null; |
| #191 | } |
| #192 | catch { |
| #193 | return null; |
| #194 | } |
| #195 | } |
| #196 | /** |
| #197 | * Fetch a URL with automatic x402 payment. |
| #198 | * If the endpoint returns 402, sign and pay, then retry. |
| #199 | */ |
| #200 | export async function x402Fetch(url, account, method = "GET", body, headers) { |
| #201 | try { |
| #202 | // Initial request |
| #203 | const initialResp = await fetch(url, { |
| #204 | method, |
| #205 | headers: { ...headers, "Content-Type": "application/json" }, |
| #206 | body, |
| #207 | }); |
| #208 | if (initialResp.status !== 402) { |
| #209 | const data = await initialResp |
| #210 | .json() |
| #211 | .catch(() => initialResp.text()); |
| #212 | return { success: initialResp.ok, response: data, status: initialResp.status }; |
| #213 | } |
| #214 | // Parse payment requirements |
| #215 | const parsed = await parsePaymentRequired(initialResp); |
| #216 | if (!parsed) { |
| #217 | return { |
| #218 | success: false, |
| #219 | error: "Could not parse payment requirements", |
| #220 | status: initialResp.status, |
| #221 | }; |
| #222 | } |
| #223 | // Sign payment |
| #224 | let payment; |
| #225 | try { |
| #226 | payment = await signPayment(account, parsed.requirement, parsed.x402Version); |
| #227 | } |
| #228 | catch (err) { |
| #229 | return { |
| #230 | success: false, |
| #231 | error: `Failed to sign payment: ${err?.message || String(err)}`, |
| #232 | status: initialResp.status, |
| #233 | }; |
| #234 | } |
| #235 | // Retry with payment |
| #236 | const paymentHeader = Buffer.from(JSON.stringify(payment)).toString("base64"); |
| #237 | const paidResp = await fetch(url, { |
| #238 | method, |
| #239 | headers: { |
| #240 | ...headers, |
| #241 | "Content-Type": "application/json", |
| #242 | "X-Payment": paymentHeader, |
| #243 | }, |
| #244 | body, |
| #245 | }); |
| #246 | const data = await paidResp.json().catch(() => paidResp.text()); |
| #247 | return { success: paidResp.ok, response: data, status: paidResp.status }; |
| #248 | } |
| #249 | catch (err) { |
| #250 | return { success: false, error: err.message }; |
| #251 | } |
| #252 | } |
| #253 | async function parsePaymentRequired(resp) { |
| #254 | const header = resp.headers.get("X-Payment-Required"); |
| #255 | if (header) { |
| #256 | const rawHeader = safeJsonParse(header); |
| #257 | const normalizedRaw = normalizePaymentRequired(rawHeader); |
| #258 | if (normalizedRaw) { |
| #259 | return { |
| #260 | x402Version: normalizedRaw.x402Version, |
| #261 | requirement: selectRequirement(normalizedRaw), |
| #262 | }; |
| #263 | } |
| #264 | try { |
| #265 | const decoded = Buffer.from(header, "base64").toString("utf-8"); |
| #266 | const parsedDecoded = normalizePaymentRequired(safeJsonParse(decoded)); |
| #267 | if (parsedDecoded) { |
| #268 | return { |
| #269 | x402Version: parsedDecoded.x402Version, |
| #270 | requirement: selectRequirement(parsedDecoded), |
| #271 | }; |
| #272 | } |
| #273 | } |
| #274 | catch { |
| #275 | // Ignore header decode errors and continue with body parsing. |
| #276 | } |
| #277 | } |
| #278 | try { |
| #279 | const body = await resp.json(); |
| #280 | const parsedBody = normalizePaymentRequired(body); |
| #281 | if (!parsedBody) |
| #282 | return null; |
| #283 | return { |
| #284 | x402Version: parsedBody.x402Version, |
| #285 | requirement: selectRequirement(parsedBody), |
| #286 | }; |
| #287 | } |
| #288 | catch { |
| #289 | return null; |
| #290 | } |
| #291 | } |
| #292 | async function signPayment(account, requirement, x402Version) { |
| #293 | const chain = CHAINS[requirement.network]; |
| #294 | if (!chain) { |
| #295 | throw new Error(`Unsupported network: ${requirement.network}`); |
| #296 | } |
| #297 | const nonce = `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")}`; |
| #298 | const now = Math.floor(Date.now() / 1000); |
| #299 | const validAfter = now - 60; |
| #300 | const validBefore = now + requirement.requiredDeadlineSeconds; |
| #301 | const amount = parseMaxAmountRequired(requirement.maxAmountRequired, x402Version); |
| #302 | // EIP-712 typed data for TransferWithAuthorization |
| #303 | const domain = { |
| #304 | name: "USD Coin", |
| #305 | version: "2", |
| #306 | chainId: chain.id, |
| #307 | verifyingContract: requirement.usdcAddress, |
| #308 | }; |
| #309 | const types = { |
| #310 | TransferWithAuthorization: [ |
| #311 | { name: "from", type: "address" }, |
| #312 | { name: "to", type: "address" }, |
| #313 | { name: "value", type: "uint256" }, |
| #314 | { name: "validAfter", type: "uint256" }, |
| #315 | { name: "validBefore", type: "uint256" }, |
| #316 | { name: "nonce", type: "bytes32" }, |
| #317 | ], |
| #318 | }; |
| #319 | const message = { |
| #320 | from: account.address, |
| #321 | to: requirement.payToAddress, |
| #322 | value: amount, |
| #323 | validAfter: BigInt(validAfter), |
| #324 | validBefore: BigInt(validBefore), |
| #325 | nonce: nonce, |
| #326 | }; |
| #327 | const signature = await account.signTypedData({ |
| #328 | domain, |
| #329 | types, |
| #330 | primaryType: "TransferWithAuthorization", |
| #331 | message, |
| #332 | }); |
| #333 | return { |
| #334 | x402Version, |
| #335 | scheme: requirement.scheme, |
| #336 | network: requirement.network, |
| #337 | payload: { |
| #338 | signature, |
| #339 | authorization: { |
| #340 | from: account.address, |
| #341 | to: requirement.payToAddress, |
| #342 | value: amount.toString(), |
| #343 | validAfter: validAfter.toString(), |
| #344 | validBefore: validBefore.toString(), |
| #345 | nonce, |
| #346 | }, |
| #347 | }, |
| #348 | }; |
| #349 | } |
| #350 | //# sourceMappingURL=x402.js.map |