tempo: signed transaction validBefore is ~45s after sign, much shorter than challenge.expires (5min)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 176
- Forks
- 66
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 52
Description
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.7and0.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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Trace the mppx sign Tempo charge path and compare it with challenge issuance in dist/tempo/server/Charge.js. Run the standalone bash reproduction, decode the credential, and compare tx.validBefore with challenge.expires; done means the signed transaction remains usable through the promised challenge window and the slow mainnet submission succeeds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- payments
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100