Repository navigation
feat(request)!: pay MPP and x402 from any Tempo stablecoin, on Tempo or Base, through machine-fetch - #290
Draft
brendanjryan wants to merge 41 commits into
Draft
feat(request)!: pay MPP and x402 from any Tempo stablecoin, on Tempo or Base, through machine-fetch#290brendanjryan wants to merge 41 commits into
brendanjryan wants to merge 41 commits into
Conversation
scripts/vendor.sh mppx|machine-fetch <repository> <ref> builds a package at one commit, from GitHub or a local clone, packs it as it would be published (mppx's own publish preparation included), and keeps its dist/, package.json, and LICENSE in vendor/<package> with a VENDORED file naming the commit. vendor/ is checked in as built: formatting and linting skip it, and its dist/ is not ignored.
wevm/mppx brendan/wallet-cli-needs @ 8295134: the mppx@0.12.0 tag plus the auto-swap allowlist (autoSwap.fallback), OUSD in the default auto-swap currencies, and auto-swap reads without the account. Those commits are merged upstream on mppx main (0.13.x) as 82810df (#1054) and 0acc2bc (#1055); no npm release has them, and wallet-cli stays on 0.12 until it chooses to upgrade.
Depend on vendor/mppx and vendor/machine-fetch, pinned by commit, through file: dependencies, which minimumReleaseAge does not apply to. A pnpm override resolves every mppx, including machine-fetch's and accounts' peers, to the vendored copy, so the install and the bundle hold exactly one; test/vendor.test.ts checks both. vendor/ stays until wallet-cli chooses to upgrade a package; re-vendoring is one scripts/vendor.sh run.
Runs the CLI bundles against anvil --network tempo (on testnet's chain ID, so the CLI's testnet network works through TEMPO_RPC_URL) and a plain anvil standing in for Base, adapted from machine-fetch's test/e2e. It deploys local OUSD and USDC with DEX books and fee AMM pools, EIP-3009 USDC on the EVM node, a wallet store with an unpublished access key, local MPP and x402 servers, and a stand-in relay that serves Relay's quote and status endpoints, settles deposits on Tempo, and releases USDC from inventory. The first scenarios pay an MPP charge in a held token through the access key, bridge through the stand-in relay, and settle an x402 authorization.
Charges go through planAndFund (src/payment/funding.ts): mppxRequirement, planPayment, then machine-fetch's Mppx extension, and the credential is created with the context it prepared. There are no funders yet, so payments are unchanged. --max-spend becomes maxPayment as an asset list: every USD token the CLI lists for the network at the cap. A charge in any other token is now refused with E_LIMIT_EXCEEDED instead of being compared as if it had six decimals. enforceMaxSpend still refuses an over-cap charge early with the same cap. machine-fetch error codes map to the CLI's codes and ctas. src/shared/chains.ts holds the trusted token table; TEMPO_DEV_CHAINS adds development tokens, which the e2e harness uses for its local chain.
A Tempo charge in a listed USD token the wallet does not hold now buys it on the Tempo DEX in the same transaction, through machine-fetch's tempoDex and Mppx auto-swap, from OUSD, then USDC.e, then pathUSD, with slippage from --max-slippage-bps (default 50). The funder reads the balance first: a wallet that holds the token gets an available receipt and no swap, and one whose balance cannot be read pays as before. Before signing it refuses a swap the access key's stored limit cannot cover (E_LIMIT_EXCEEDED with the keys update cta) or no configured input can cover (E_INSUFFICIENT_FUNDS with the fund cta), since Mppx appends its own inputs. MACH settlement never swaps. --funding auto|tempo|off, TEMPO_FUNDING, and the funding config setting choose the mode; auto behaves as tempo until cross-chain funding lands. The receipt and --write-meta name the swap's input and maximum, and --dry-run reports the funder when the balance is short. The e2e harness pays an MPP charge in USDC from OUSD on the local chain.
tempo request now considers MPP evm charges and x402 exact offers (unless --funding off), on a chain in the destination table and in its USDC: Base for mainnet, Base Sepolia for testnet, plus TEMPO_DEV_CHAINS destinations. An offer elsewhere, in another token, or for another EIP-3009 domain is refused before signing. Mppx's evm method signs exactly the selected offer. The payer is the wallet's remote payer (D2): one secp256k1 key per wallet, created on first use under a file lock and kept in remote-payers.json (0600) beside the wallet store, which the CLI and the accounts SDK rewrite whole. The key never appears in output; whoami reports its address and USDC. A --private-key signer pays for itself. Offers are ranked deterministically: a Tempo offer in a held token, a Tempo offer a DEX swap funds, an EVM offer the payer already funds, then (with --funding auto) one to bridge; --payment-network overrides. Balances are read only when both kinds are offered. An unfunded payer is refused before signing with its address. --dry-run quotes EVM offers without creating a payer.
With --funding auto (or TEMPO_EXPERIMENTAL_CROSS_CHAIN=1) an EVM offer the remote payer cannot cover is funded by machine-fetch's crossChainFund: exact output from OUSD on Tempo to USDC at the payer, through Relay deposit addresses, with operations in machine-fetch's file store. src/payment/upstream/relay-route.ts (MF-2) supplies relayProtocol's guarantees, submit, and status: a quote must have one transaction step that transfers OUSD on Tempo, from the wallet, to the step's deposit address, for the quoted input up to slippage, and must deliver exactly the shortfall of the destination USDC to the payer. submit revalidates the saved quote and sends only that transfer through the access key. --max-funding-spend caps the OUSD spend, gas included, and is refused before signing. The attempt ID is saved under ~/.tempo/wallet/funding/attempts before funding, keyed by the request (method, URL, body) and the payment terms (src/payment/upstream/requirement-key.ts, MF-14), and passed with the payer to extension().preparePayment (MF-17, MF-18). A rerun reuses it, so funding a crash interrupted is reconciled, not repeated; a new attempt first settles earlier ones to the same destination and payer. RECOVERY_REQUIRED maps to E_PAYMENT_OUTCOME_UNKNOWN with a recover cta. tempo wallet funding list|recover show and reconcile saved funding without sending anything. The e2e matrix bridges an x402 payment through the stand-in relay, rejects eight hostile quotes before signing, recovers after SIGKILL mid-bridge without a second deposit, and settles a refund.
… Stargate
Relay rejects strict deposit addresses from Tempo ("strict is not supported for
this origin chain"), so cross-chain funding could never run on mainnet. The
Relay route now asks for a contract deposit and accepts only an approve of the
source token to Relay's pinned v2 depository (0x4cD0...BC31, matching Relay's
chains API) and depositErc20(wallet, token, amount, protocol.v2.orderId),
re-encoded from the checked values and sent atomically.
A Stargate route bridges USDC.e to native USDC: it sizes amountLD so the
receipt covers the exact output, pays LayerZero's fee in LZD, and sends the
fee wrap, approvals and sendToken in one Tempo transaction, tracking delivery
through LayerZero Scan. Its fee counts toward amountIn, since machine-fetch
ranks routes by amountIn alone (MF-22).
Each payment funds from USDC.e (Relay and Stargate) or OUSD (Relay), whichever
the wallet holds enough of; a resumed attempt reconciles from the token it was
submitted in. Also: --funding off with --payment-network <evm chain> pays that
chain from held USDC instead of the Tempo offer, missing balances are reported
before quoting, and route refusal reasons are shown.
Tested by unit hostile-quote suites for both routes, a Docker e2e matrix with
a local depository and Stargate (23/23), and a quote-only mainnet probe
(scripts/probe-funding-routes.mjs).
A bridge refused before any operation was claimed (over --max-funding-spend, no route, no source balance, or another attempt still in flight) left its attempt record behind, listed as not_started by 'funding list'. Attempts with a claimed operation are still always kept for recovery.
… cheapest route Both routes now check the source balance against the quoted debit before anything is signed. Before, a wallet holding enough to pass the rough pre-quote check but less than the quote would sign a deposit that reverted on chain, which the durable funder must treat as possibly having moved funds. Such a refusal is reported as E_INSUFFICIENT_FUNDS with the amount needed and held. When routes are refused by --max-funding-spend, the error now reports the cheapest route's requirement; it reported whichever route refused first, which could be Stargate's 0.288 instead of Relay's 0.051.
The Tempo DEX funder took the first input the wallet held enough of, and only checked the access key's limit for it when the store recorded one. A key whose limits list leaves a token out cannot spend it on chain, so a wallet holding only pathUSD under a key limited to OUSD and USDC.e signed a swap the chain refused, reported against the payment token instead of pathUSD. The funder now skips inputs the key may not spend: the stored limit when the store has one, else the chain's (the remaining limit of a published limited key, and zero for a token an unpublished key's signed limits leave out). When no spendable input is held it refuses before signing and names the token, and the swap's inputs are pinned to the chosen one so Mppx cannot pick another. Fixes Phase 2 behavior (#277); found by the live mainnet run.
…eystore The remote payer's key was kept in its own remote-payers.json because the CLI rewrote store.json whole, dropping anything it did not know. It is now created by the wallet's secp256k1 keystore (Keystore.secp256k1().createKey()) and its handle is stored in store.json as the tempo-cli.remotePayers item, written through the accounts SDK's filesystem storage, alongside the access keys. saveWalletState now keeps every other item in the store, so CLI rewrites and logout keep the payer; the SDK already rewrites only the item it sets. Because the store has several writers across processes and no shared lock, and losing the key strands its funds, remote-payers.json stays as a write-once backup: written at creation, never rewritten or removed, and used to restore any payer the store loses. An existing file is migrated into the store on first use. keys list now shows the active wallet's remote payer.
….3 API Vendors machine-fetch main (cec3535, past d168943), whose API moved to namespaced modules on flat subpaths (Payment, Funder, Policy, Errors, MppxFetch, CrossChainFunder, CrossChain, Evm, RelayProtocolRoute, FileStore), error classes instead of codes, and operationId instead of attemptId. The CLI is migrated without changing what it pays or how it funds: - fundingError maps by error class (Policy.LimitExceededError, Funder.NoRouteError, ...) and reports each error's shortMessage. - Payment.plan / MppxFetch.extension take flat policy options; the extension runs with autoSwap: false, so its new built-in Tempo auto-swap never applies. The CLI's own Tempo DEX funder (now CLI-owned, since tempoDex was removed upstream) still swaps only from inputs the access key may spend. - Cross-chain requests carry operationId; token accounts name their owner. - The CLI's Relay and Stargate routes stay: upstream's new RelayProtocolRoute with viem clients is a follow-up to evaluate against Tempo contract deposits. Verified: 3,099 unit tests, test types, lint, docs, e2e 24/24, and the mainnet quote-only probe (Relay from USDC.e and OUSD, Stargate).
Picks up machine-fetch #83 (reject the removed nested policy option), #84 (enforce maxFundingSpend on every Tempo route; keep the built-in Tempo context out of a CLI extension's compose), and #85/#86 (Viem-backed Relay route hardening, unused by the CLI). No CLI changes needed: 3,099 unit tests, e2e 24/24, and the mainnet quote-only probe pass.
…ing minutes, check the swap's remaining key limit, record x402 settlement hashes - A bridge's completed status waits for the destination balance to show the delivery; Relay and LayerZero report success before Base's RPC reflects it, which surfaced a delivered bridge as E_PAYMENT_OUTCOME_UNKNOWN. - Cross-chain funding gets a 300 s deadline instead of machine-fetch's 30 s default; a Stargate send took 23 s to deliver live. - wallet swap checks the access key's on-chain remaining limit, not the stored grant, and refuses with the keys update cta instead of falling back to wallet-app approval. The funder's spend check uses the same reading. - EVM and x402 receipts record the settlement transaction from payment-response (or an MPP evm payment-receipt).
…ment hashes on x402 receipts
…-integration # Conflicts: # README.md # SKILL.md # test/mcp-services.test.ts
This was referenced Oct 20, 2026
Closed
…saction A run killed after paying but before recording it leaves the operation submitted, and the payer's balance back at zero, so 'funding recover' reported pending forever. Relay and Stargate now report the destination transactions of a completed operation, and a completion counts once the balance covers the payment or one of those transactions transferred the output to the payer.
A run killed between broadcasting a Stargate send and recording its hash left recovery to a human (action_required). Each quote now saves the Tempo block it was made at, and recovery without a hash looks for the wallet's one OFTSent of exactly that amount and endpoint since then. e2e: the local Stargate mock emits OFTSent; new cases cover that recovery, a server that refuses the payment after the bridge delivers (the next request pays from the delivery), and a run killed after paying but before recording it (recover confirms the spent delivery by its fill).
…-only offers with --funding off
- A Tempo DEX swap's approve and the DEX's pull both count against the access key's limit, so a
key whose limit covered the input once passed the checks and then reverted in simulation with
an opaque E_PAYMENT. The DEX funder and wallet swap now require twice the input, and the error
says why.
- --funding off on a server that offers only x402 (or MPP evm) failed with "missing
WWW-Authenticate header"; it now pays from USDC the payer already holds, never bridging.
- A route whose provider cannot be reached now says so ("Relay is unreachable (…)").
e2e: partial payer balance bridges only the difference, no-route refusal, --funding off/tempo never
bridge, a bridged --dry-run moves nothing, nothing held covers the swap, --max-spend refusal,
wallet swap against the on-chain remaining limit, and the double-counted swap limit.
…o send reported failed
…owed the delivery Two live bridges ended in E_PAYMENT_OUTCOME_UNKNOWN although Relay delivered. mainnet.base.org serves a transaction's receipt before 'latest' reads include its block (Flashblocks preconfirmations), so the delivery check saw the fill while machine-fetch's single balance read, moments later, did not. The delivery check now records the block that showed the delivery, and the funder's balance read waits for the RPC to reach that block and reads at it. e2e: a stand-in RPC that serves receipts before state; the case fails without this change.
…-integration # Conflicts: # src/request/dry-run.ts
…holds enough of it The SDK pays a machine-enabled charge with MACH before the advertised token. Stablecoin funding ran first, so a wallet holding MACH but not the token either swapped OUSD for nothing or was refused for lack of stablecoins. Funding now steps aside while MACH covers the charge, and --dry-run previews no swap then.
SKILL.md explains paying from other tokens and chains: the --funding modes, the remote payer, approving a bridge's fee, the access-key limit a DEX swap needs, and reconciling E_PAYMENT_OUTCOME_UNKNOWN with funding recover. The stack's changelog entries are consolidated to describe the final behavior against main, and --max-funding-spend's help names its source token.
`tempo request --dry-run` on an x402 or MPP `evm` offer the remote payer could not cover reported no `funding`, so the only way to see what a bridge would cost was a real request refused by `--max-funding-spend 0`, which pays outright when the payer already holds the price. With `--funding auto`, the dry run now quotes the routes the payment would use for the payer's shortfall, from the first Tempo source token holding enough, and picks as machine-fetch picks: the cheapest quote within `--max-funding-spend`, else the cheapest one so the cap it needs is visible. `funding` reports the shortfall, route, source token, `amount_in`, fee, gas, total, the cap and whether it covers the total, and every route's quote or why it declined. Nothing is signed, no attempt is claimed, and no operation is saved; a wallet without a remote payer is quoted to its Tempo address and still gets none. e2e: the bridged dry run reports the stand-in Relay's quote and moves nothing.
…d of vendoring them machine-fetch 0.3.1 is its main at bafeca5, the same build vendor/ held (dist is byte-identical). mppx 0.13.6 is the first release with the auto-swap allowlist (#1054) and reads without the account (#1055). Both are excluded from the 7-day release-age gate, like accounts@0.21.0. One mppx stays in the install and the bundle (test/mppx-install.test.ts). vendor/, scripts/vendor.sh, and the scanner and formatter exceptions for it are removed.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. Ignoring alerts on:
|
…-integration # Conflicts: # pnpm-lock.yaml
0.3.2 carries the five cross-chain fixes upstreamed from this integration: a 300-second default deadline, polling the destination balance until the deadline, confirming a spent delivery from its destination transactions, saving a reference before broadcasting, and budgeting an access key for both spends of an auto-swap. CrossChainStatus now has destinationTxs, so the CLI's own DeliveredStatus field is dropped in favor of it.
machine-fetch 0.3.2 gives a cross-chain funder its own 300-second default deadline when policy sets none (tempoxyz/machine-fetch#88), so the CLI no longer overrides `deadlineSeconds` for a bridged payment. The dry-run preview quotes with the funder's own default. A test runs a payment through machine-fetch with a policy that sets no deadline and checks the bridge's quote gets 300 seconds.
machine-fetch 0.3.2 polls the destination balance until the deadline after a route completes (tempoxyz/machine-fetch#89) and, in `recover`, confirms a delivery a payment already spent from the route's `destinationTxs` (tempoxyz/machine-fetch#90). That covers what the CLI wrapped every route in, so this drops: - `confirmedDelivery`, which turned `completed` into `pending` until the delivery was visible. - `DeliveryFloor` and `destinationBalanceAt`, which kept machine-fetch's single balance read off a lagging node by reading no earlier than the block that showed the delivery. - The `target` plumbing, and the `DeliveredStatus` alias of `CrossChain.Status`. The funders get the CLI's destination readers instead: `balance` reads the payer's USDC at "latest", and `destinationDelivery`, now returning only the amount, is the `delivered` reader. `funding recover` and the reconciliation before a new funding still ask the route first, so refunded and pending operations read as before, then confirm a completion with the funder's `recover`; one not yet visible stays pending. Base's read lag (mainnet.base.org serves receipts before "latest" state) is still covered: in the lagging-RPC e2e test, logging each read showed the first three balance reads after completion returned 0 and the fourth the delivery, so machine-fetch's polling alone is enough. Unit tests run each case the wrapper handled through machine-fetch with the CLI's readers.
machine-fetch 0.3.2 hands a route `saveReference` to save its reference before broadcasting (tempoxyz/machine-fetch#91). The Stargate route now signs its send, saves the signed transaction's hash, and only then broadcasts it, so a run killed in between still leaves the hash for recovery. A save that fails stops the send, and a broadcast whose hash differs from the one saved is rejected. A saved send may never land, so the route signs it valid only until the quote expires (Tempo's `validBefore`) and reports one with no receipt as refunded once Tempo is a minute past that. New quotes no longer record the source block, so the `OFTSent` log search only runs for operations saved before this, which recorded the hash after broadcasting. The e2e test for a never-recorded hash now rewrites the saved operation to that earlier shape, and the kill mid-bridge test checks that the hash saved is the send's own.
#290 ships as one PR, so the copies of machine-fetch behavior it replaced never released; the cross-chain funding entry already describes the final behavior.
…utes Depend on machine-fetch 0.4.1 and use its RelayProtocolRoute (contract deposits with the Relay order recomputed and checked, the v2 depository pinned) and StargateRoute, signing through the access-key Tempo client and confirming deliveries through a Base client. Both refuse a quote the wallet cannot cover before signing. The CLI keeps only its progress lines, as thin wrappers around each route's submit and status. wallet-cli's own relay-route.ts, stargate-route.ts and funding-limit.ts go: refusals map through machine-fetch's Funder.FundingLimitError and Funder.SourceBalanceError via Funder.cheapestRefusal. The quote-only probe now runs machine-fetch's routes against mainnet. Dogfooding this found that every bridge failed with an unpublished access key, a fresh login's until its first payment; fixed upstream in tempoxyz/machine-fetch#105 (0.4.1).
…te routes - The stand-in Relay serves a /chains registry and real orders whose ID is the order's EIP-712 hash, as machine-fetch recomputes it; its expired mode expires the order itself, and its hostile recipient tampers with the order. - The stand-in LayerZero Scan names the destination transaction of a delivery. - The cap and preview cases match machine-fetch's gas estimate instead of a fixed 0.02 reserve. - Two cases go: Relay reporting success before naming its fill (machine-fetch requires the fill; the lagging-RPC case covers the live failure) and finding a send saved without its hash (an interim format that never shipped; the hash is now saved before broadcast).
Contributor
Author
|
@SocketSecurity ignore npm/machine-fetch@0.4.1 npm/mppx@0.13.6 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integrates machine-fetch into
tempo request, so the wallet can pay MPP and x402 challenges in a token it doesn't hold, or on another chain (Base first), funded from its Tempo stablecoins. This replaces the nine-PR stack (#274–#279, #284, #285, #287) with one PR. The commits are kept in stack order, so it still reads layer by layer.What it does
src/payment/funding.ts).--max-spendbecomes machine-fetch's payment limit over the USD tokens the CLI lists. Behavior change: a charge in an unlisted token is refused withE_LIMIT_EXCEEDEDinstead of being treated as six-decimal.--funding auto|tempo|off,TEMPO_FUNDING, configfunding, and--max-slippage-bps. The CLI owns this funder (src/payment/tempo-funding.ts) and passesautoSwap: falseto machine-fetch, so--funding offnever swaps.evmcharges and x402exactoffers in USDC on Base (Base Sepolia on testnet) are paid from a per-wallet secp256k1 payer key.store.jsonastempo-cli.remotePayers.remote-payers.jsonis a write-once backup.--payment-networkoverrides the ranking.whoamiandkeys listshow the payer.--funding auto, it bridges exactly the shortfall to the payer, within--max-funding-spend. It picks the cheapest route the wallet can cover, through machine-fetch's own routes:RelayProtocolRoute) from USDC.e or OUSD: a contract deposit into Relay's pinned v2 depository. machine-fetch recomputes and checks the Relay order before signing.StargateRoute) from USDC.e: the OFTsendTokencall, with LayerZero's fee wrapped into LZD in the same transaction.FundingLimitErrorandSourceBalanceError.tempo wallet funding listandtempo wallet funding recover <id>inspect and reconcile attempts.--funding auto,--dry-runon a Base offer the payer can't cover quotes the real Relay and Stargate routes.fundingreports the shortfall, the route, the source token,amount_in, the fee, gas, whether--max-funding-spendcovers it, and each route's quote or why it declined. Nothing is signed or saved.Fixes from the live mainnet runs
E_PAYMENT_OUTCOME_UNKNOWN; a stand-in RPC reproduces that in the e2e.action_required. A saved send that never landed counts as refunded once it expires.formatTokenUnitsandformatCreditBalanceproduce malformed output for negative BigInt values #88), instead of the 30 s payment default.approveand once for the DEX's pull. A key whose limit covered the swap once used to pass the checks and then revert with an opaqueE_PAYMENT. The DEX funder andwallet swapnow require twice the input, read the on-chain remaining limit rather than the stored grant, and explain the refusal.--funding off: a server offering only x402 used to fail with "missing WWW-Authenticate header". It now pays from USDC the payer already holds, and never bridges.payment-responseor an MPPevmreceipt.Verified
main.pnpm test:e2e: 39 of 39 on local Docker chains (anvil Tempo plus an EVM chain, with stand-in Relay, Stargate and LayerZero Scan). It covers:evmpayments;scripts/probe-funding-routes.mjs). Relay from USDC.e costs about $0.021–0.024; LayerZero's fee for Stargate rose from about $0.26 to $1.26 on 2026-10-20.Upstream
RelayProtocolRouteis out of scope.Storage.filesystem, which would let the backup file go) and accounts-2 (an EVM signer from a keystore handle) are still open.Security review requested for the remote payer key handling (
src/wallet/remote-payer.ts,src/wallet/store.ts) and the route call validation (src/payment/upstream/).Dogfooding found
Switching to machine-fetch's own routes found that every bridge failed with an unpublished access key, the state of a fresh login until its first payment. Its reads went out from the key, which the chain rejects (
KeyNotFound), and its gas quote left out the cost of publishing the key. Both are fixed upstream in tempoxyz/machine-fetch#105, released as 0.4.1.