Repository navigation
Conversation
paid_call moved money from a live key and left no receipt: the wrapped x402 fetch returns a plain Response, and the price only exists on the request the transport makes. PaymentLedger appends one record per attempt to MCP_PAYMENT_LEDGER_FILE - failed and rejected calls included, since a payment can settle on a call that errored - and never reads the signing key or the signed envelope out of the payment header. Each record says whether its spend is confirmed (settled), unprovable (possible) or excluded (none), so the trail reads as an upper bound instead of inventing certainty a seller never confirmed. An audit file that cannot be created, or that a record cannot reach, makes paid_call refuse further calls rather than spend unrecorded. The same records count into mcp_payment_attempts_total, mcp_payment_amount_usd_total and mcp_payment_unquantified_total. Their host label may only take a value from the paid_call allow-list - the same list that decides what can be paid at all - and their network label only the two networks this signer can use, so no caller mints permanent time series with the URLs it tries. totalSpend() gives a cumulative ceiling the running sum to read.
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.
Summary
paid_callsigns with a live mainnet key, and until now nothing said what it spent: the wrapped x402 fetch hands back a plainResponse, and the price the agent agreed to is only visible on the request the transport makes. This adds the receipt.src/mcp/ledger.ts—PaymentLedger. One JSON record per attempt is appended toMCP_PAYMENT_LEDGER_FILE(default~/.lens-mcp/payments.jsonl): timestamp, full URL, host, method, price/asset/network exactly as the seller offered them, settlement hash, outcome, spend class, and the error code the agent was shown. Failed and refused calls are recorded too — a payment can settle on a call that errored, so a log of successes is not an audit trail.settledwhen the seller's settlement header confirms it,possiblewhen a payment was offered and nothing proves it failed,nonewhen something proves nothing moved.transactionHashisnullwhenever the resource does not echo its settlement, so the trail reads as an upper bound instead of inventing certainty.accepted.{amount, asset, network}off the payment header and deliberately neverpayload, the signed transaction envelope; the signing key never enters the class. A test carries a decoy envelope and asserts its marker appears in neither a record nor the file.paid_callrefuses further calls withERR_INTERNAL_FAILUREbefore sending a request. A tool that moves money does not get to spend without leaving a record.mcp_payment_attempts_total{host,network,outcome},mcp_payment_amount_usd_total{host,network}andmcp_payment_unquantified_total{host,network}on the shared registry. Series are permanent, sohostmay only take a value from the paid-call allow-list feat: add configurable host allow-list to paid_call, refuse before any request #487 added — the same list that decides what can be paid at all, so there is no second knob to drift — andnetworkonlystellar:testnet/stellar:pubnet, the two networks this signer can use. Anything else is labelledunknown; the audit file always keeps the real host.totalSpend()returns attempts, settled spend and unconfirmable spend as integer nanodollars in decimal strings (no float drift), plus a count of payments it could not price in USD — so the sums announce when they understate.What this deliberately does not do
npm run mcp:serverprints where the audit file is at startup, so an operator always knows where the record lives.totalSpend()is what this process recorded since boot. Reading the file back across restarts and spending against a cumulative cap is The spend cap is per call, so it is not a cap #405's work; this is the measurement it needs, not the cap.bazaar_searchis not recorded. Discovery never pays, so it has no place in a spend trail.Related issue
Closes #406
Maintainer note: the open #522, same author, also lists
Closes #406, and its branch was cut before #487/#488 landed. This PR is only the #406 slice, re-based on currentmain(94bd8ea). If you take it, #522 should be narrowed to #405/#419/#421/#425 rather than merging the same audit ledger twice. Closed-unmerged #491 and the open #447 covered this area too; nothing here re-implements their budget or HTTP work.Type of change
Checklist
npx tsc --noEmitpassesnpm run buildpassesTests
npx vitest run tests/mcp.test.ts tests/mcp-paid-call-allowlist.test.ts tests/x402-paid-call-loop.test.ts tests/httpMetrics.test.ts→ 49 passed (21 of them intests/mcp.test.ts).Each acceptance criterion, with the test that names it:
records every paid call, whatever the transport did with it— one server, four transports (success, refused 402, 500 after the payment loop, a thrownfetch failed), four records, spend['settled', 'none', 'possible', 'possible']records a failed payment as an attemptrecords the transaction hash the seller echoes on settlementexports spend as metrics with a bounded label set— the allow-listed series, theunknownfallback, and that a host outside the allow-list never becomes a series of its ownrecords no secret material from the payment header,appends one line per attempt to the audit file, oldest firstrefuses every paid call when the audit file cannot be created,stops spending once a record fails to reach the audit file— both trigger a realmkdir/appendfailure rather than a flag set by the testsums settled spend and flags spend it cannot priceThe block is entirely offline: DNS and the transport are injected, and every audit file is created under
mkdtempSyncand removed afterwards.Two failures you will see that are not from this diff: the prices load test already fails on
main, andexamples/mcp-server/run.tsreports an excess-property error onmaxPriceagainst the installed@x402/fetchtypes —tsconfig.jsononly includessrc/**, so neither CI'stscnornpm run buildreaches it, butnpm run mcp:servertypechecks it.Files
8 files, +1074/−22:
src/mcp/ledger.ts(new),src/mcp/server.ts,examples/mcp-server/run.ts,tests/mcp.test.ts,examples/mcp-server/README.md,README.md,.env.example,.changeset/mcp-payment-audit.md(minor).