Summary
When mppx sign produces a transaction-type credential for a tempo.charge challenge, the underlying Tempo transaction's validBefore is set to roughly 45 seconds after sign time, regardless of the challenge's expires field (typically 5 minutes out per the mppx server defaults). On Tempo mainnet, this causes the signed transaction to be rejected on-chain (current_block_timestamp >= validBefore) whenever the round-trip from sign → submit → server verify exceeds ~45 seconds.
This is invisible in fast end-to-end tests but consistently breaks agent-driven MCP flows, where each tool call (challenge → mppx sign → submit) easily takes 30-90 seconds of agent overhead.
Environment
- mppx CLI:
0.6.5
- mppx server lib: tested both
0.5.7 and 0.6.5 — same symptom, different surfaced error class
- Tempo network: mainnet (chainId
4217, https://rpc.tempo.xyz)
- Tempo network: testnet (chainId
42431, https://rpc.moderato.tempo.xyz) — does NOT exhibit this (transaction validity is more lenient or block timestamp progresses differently)
Repro
Standalone bash repro using mppx sign + curl against any mppx-protected endpoint that returns a Tempo charge challenge with a 5-minute expires:
# 1. Get challenge (any mppx-server-protected URL on Tempo mainnet)
CHAL=$(curl -sS https://your-server/path | extract WWW-Authenticate)
echo "$CHAL"
# → Payment id="...", method="tempo", expires="2026-04-26T15:05:04.853Z" ...
# (challenge expires ~5 minutes from now)
# 2. Sign immediately
CRED=$(npx mppx sign --account my-wallet --challenge "$CHAL")
# 3. Decode the signed transaction's validBefore
node -e '
import { Credential } from "mppx";
import { Transaction } from "viem/tempo";
const c = Credential.deserialize(process.argv[1].replace(/^Payment /, ""));
const tx = Transaction.deserialize(c.payload.signature);
console.log("challenge.expires:", c.challenge.expires);
console.log("tx.validBefore (unix):", tx.validBefore);
console.log("tx.validBefore (date):", new Date(Number(tx.validBefore) * 1000).toISOString());
console.log("delta sign-time → validBefore:", (Number(tx.validBefore) - Math.floor(Date.now()/1000)), "seconds");
console.log("delta validBefore → challenge.expires:", (Math.floor(Date.parse(c.challenge.expires)/1000) - Number(tx.validBefore)), "seconds");
' "$CRED"
Sample output observed in production:
challenge.expires: 2026-04-26T15:05:04.853Z
tx.validBefore (unix): 1777215649
tx.validBefore (date): 2026-04-26T15:00:49.000Z
delta sign-time → validBefore: ~45 seconds
delta validBefore → challenge.expires: ~255 seconds (the unused window)
Symptom on mainnet
When the credential is submitted to a server that runs tempo.charge verify(), the server's pre-flight viem_call simulation fails with the raw RPC error:
Revm error: transaction expired: current block timestamp 1777216094 >= validBefore 1777215649
(viem wraps this as CallExecutionError: An internal error was received in 0.6.5, or InvalidInputRpcError: Missing or invalid parameters in 0.5.7.)
The actual Tempo mainnet RPC is healthy — eth_chainId and eth_blockNumber work fine. The chain just refuses to simulate / accept a transaction whose validBefore is in the past.
Confirming it's purely timing
A fast-path repro succeeds first try:
# Fast (4 seconds end-to-end, mainnet, fresh wallet) → succeeds
T=$(date +%s); CHAL=$(curl ...); CRED=$(mppx sign ...); curl -d {credential=$CRED}
# → {"paid": true, ...}, ~$0.01 USDC settles on chain
A slow-path repro (sign, wait 60s, submit) fails with transaction expired. Same code, same wallet, same RPC.
Root cause (best guess)
In dist/tempo/server/Charge.js, the challenge issuance side uses defaults.expires or a 5-minute window. But on the client side, tempo.charge (or whatever mppx CLI calls under the hood) appears to set transaction validBefore to a much shorter window — possibly hardcoded to a default like now + 60s rather than challenge.expires.
I haven't dug into the CLI's signing code path, but the credential's TX consistently shows validBefore ~45s after sign-time across many test signs, regardless of how far out challenge.expires is.
Suggested fix
In mppx sign (or wherever the Tempo transaction is constructed when responding to a charge challenge), set tx.validBefore to Math.floor(Date.parse(challenge.expires) / 1000) — i.e., let the transaction be valid for the entire window the server already promised. The challenge expiry is the upper bound the server committed to honoring; the TX validity should match.
Alternative: make the validBefore window configurable via a mppx sign --valid-for <seconds> flag and/or MPPX_VALID_FOR env var.
Impact
- Severity: high for any agent-driven flow on Tempo mainnet
- Workarounds: retry the sign+submit cycle (sometimes works if the second attempt is faster); or use the Stripe SPT rail (no on-chain expiry)
- Not affected: testnet (
chainId 42431), fast direct HTTP integrations
Happy to test a fix or contribute a PR if helpful.
Summary
When
mppx signproduces atransaction-type credential for atempo.chargechallenge, the underlying Tempo transaction'svalidBeforeis set to roughly 45 seconds after sign time, regardless of the challenge'sexpiresfield (typically 5 minutes out per the mppx server defaults). On Tempo mainnet, this causes the signed transaction to be rejected on-chain (current_block_timestamp >= validBefore) whenever the round-trip from sign → submit → server verify exceeds ~45 seconds.This is invisible in fast end-to-end tests but consistently breaks agent-driven MCP flows, where each tool call (challenge → mppx sign → submit) easily takes 30-90 seconds of agent overhead.
Environment
0.6.50.5.7and0.6.5— same symptom, different surfaced error class4217,https://rpc.tempo.xyz)42431,https://rpc.moderato.tempo.xyz) — does NOT exhibit this (transaction validity is more lenient or block timestamp progresses differently)Repro
Standalone bash repro using
mppx sign+ curl against any mppx-protected endpoint that returns a Tempochargechallenge with a 5-minuteexpires:Sample output observed in production:
Symptom on mainnet
When the credential is submitted to a server that runs
tempo.chargeverify(), the server's pre-flightviem_callsimulation fails with the raw RPC error:(viem wraps this as
CallExecutionError: An internal error was receivedin0.6.5, orInvalidInputRpcError: Missing or invalid parametersin0.5.7.)The actual Tempo mainnet RPC is healthy —
eth_chainIdandeth_blockNumberwork fine. The chain just refuses to simulate / accept a transaction whosevalidBeforeis in the past.Confirming it's purely timing
A fast-path repro succeeds first try:
A slow-path repro (sign, wait 60s, submit) fails with
transaction expired. Same code, same wallet, same RPC.Root cause (best guess)
In
dist/tempo/server/Charge.js, the challenge issuance side usesdefaults.expiresor a 5-minute window. But on the client side,tempo.charge(or whatever mppx CLI calls under the hood) appears to set transactionvalidBeforeto a much shorter window — possibly hardcoded to a default likenow + 60srather thanchallenge.expires.I haven't dug into the CLI's signing code path, but the credential's TX consistently shows
validBefore~45s after sign-time across many test signs, regardless of how far outchallenge.expiresis.Suggested fix
In
mppx sign(or wherever the Tempo transaction is constructed when responding to a charge challenge), settx.validBeforetoMath.floor(Date.parse(challenge.expires) / 1000)— i.e., let the transaction be valid for the entire window the server already promised. The challenge expiry is the upper bound the server committed to honoring; the TX validity should match.Alternative: make the validBefore window configurable via a
mppx sign --valid-for <seconds>flag and/orMPPX_VALID_FORenv var.Impact
chainId 42431), fast direct HTTP integrationsHappy to test a fix or contribute a PR if helpful.