Tempo payment method — verifies payment via on-chain TIP-20 token transfer.
Tempo supports two credential types:
type="hash"— Client already broadcast the transaction; server verifies the receipt via RPC (eth_getTransactionReceipt).type="transaction"— Client sends a signed Tempo Transaction (0x76); server decodes, optionally adds fee payer signature, broadcasts, and verifies.
Configuration
Pass Tempo-specific config via :method_config in MPP.Plug opts:
plug MPP.Plug,
secret_key: "hmac-secret",
realm: "api.example.com",
method: MPP.Methods.Tempo,
amount: "1000000",
currency: "0x20c0000000000000000000000000000000000000",
method_config: %{
"rpc_url" => "https://rpc.moderato.tempo.xyz",
"chain_id" => 42431,
"fee_payer" => false
}Config Keys
"rpc_url"— (required) Tempo RPC endpoint URL"chain_id"— (optional) network chain ID, defaults to42431(Moderato testnet)"fee_payer"— (optional) enable server-side fee sponsorship, defaults tofalse. Whentrue, the server co-signs client transactions with domain0x78to pay transaction fees. Requires"fee_payer_private_key"and"fee_token", unless"fee_payer_url"is set for hosted sponsorship."fee_payer_url"— (optional) URL of a hosted fee-payer service that implementseth_fillTransaction. When set, the server delegates co-signing to the remote endpoint instead of using"fee_payer_private_key". Mutually exclusive with the local key path."fee_payer_private_key"— (required whenfee_payer: trueand nofee_payer_url) hex-encoded 32-byte secp256k1 private key for the fee payer account"fee_token"— (required for local co-sign whenfee_payer: trueand nofee_payer_url) hex address of a USD-denominated TIP-20 token to use for fee payment (e.g., pathUSD). Must be on the sponsor allowlist (fee_payer_allowed_fee_tokensor per-chain defaults)."fee_payer_allowed_fee_tokens"— (optional,fee_payer: trueonly) list of hex addresses overriding the default sponsor fee-token allowlist."fee_payer_policy"— (optional,fee_payer: trueonly) map of sponsor ceilings overriding the per-chain defaults:"max_gas","max_fee_per_gas","max_priority_fee_per_gas","max_total_fee"(wei), and"max_validity_window_seconds"(seconds). Bounds the client-supplied gas fields and validity window before the server co-signs so a malicious client cannot drain the fee-payer wallet via inflated gas price, total fee budget, or a padded access list, nor hold a co-signed sponsorship broadcastable far into the future. SeeMPP.Methods.Tempo.FeePayerPolicy."memo"— (optional) bytes32 hex memo fortransferWithMemo"wait_for_confirmation"— (optional) whenfalse, broadcasts without waiting for on-chain confirmation. Pre-simulates the full co-signed transaction viaeth_simulateV1first (same guard as the default path) to reject a tx that would revert before broadcast, then broadcasts async and returns an optimistic receipt. Defaulttrue."store"— (optional) replay-dedup store. On by default — when absent, the app-startedMPP.Tempo.ConCacheStoreis used so replay protection is enabled out of the box (issue #7). Pass a module implementingMPP.Tempo.Store(Redis, Postgres, etc. — for multi-node deployments) or{MPP.Tempo.ConCacheStore, opts}to configure the built-in store (for example a custom cache:name). A configured store MUST implement the atomiccheck_and_mark/2. Passstore: falseto explicitly opt out of dedup (not recommended; incompatible with a static"memo")."require_presenter_binding"— (optional) requiretype="hash"andtype="transaction"credential presenters to prove control of the transfer's sender wallet via a"presenterSignature"payload field (see Presenter binding below). Defaults tofalse.
Credential Payload
The credential payload map must contain one of:
"type" => "hash","hash" => "0x..."— transaction hash for receipt verification"type" => "transaction","signature" => "..."— RLP-serialized signed Tempo Transaction
Both may additionally carry "presenterSignature" => "0x..." (see below).
Presenter Binding
On the hash/transaction paths, dedup is keyed on the tx hash alone — nothing in the
base protocol proves the credential presenter controls the wallet that broadcast
the transfer, so a third party who observes a settled transfer can race its hash
against their own fresh challenge (the residual documented in GHSA-34g7-vx6g-82mq).
Setting "require_presenter_binding" => true closes that race: the presenter must
include "presenterSignature", an EIP-712 signature over the same Proof typed
data the type="proof" path uses (domain {name: "MPP", version: "3", chainId},
struct {account, challengeId, realm} — see MPP.Methods.Tempo.Proof), signed by
the transfer sender's wallet or one of its authorized access keys.
type="hash"— the credential's top-levelsource(adid:pkh:eip155:DID) is required and names the account; the signature must recover to it, and the matched transfer'sfrommust equal it.type="transaction"— the account is the sender recovered from the signed transaction itself; asource, when present, must match it.
When the flag is off (default) a supplied "presenterSignature" is still verified
(an invalid one is rejected), so compliant clients can send it unconditionally.
The requirement is advertised to clients as "presenterBinding" => true in the
402 challenge's method details.
This is a deliberate hardening extension beyond the reference SDKs: neither mpp-rs
nor mppx binds the presenter on the hash path (both default the expected sender to
the receipt's from — refs/mpp-rs/src/protocol/methods/tempo/method.rs verify_hash,
refs/mppx/src/tempo/server/Charge.ts hash branch), which is why it is opt-in.
Dependencies
Requires the onchain and onchain_tempo packages for RPC calls, transfer log
parsing, and 0x76 Tempo transaction handling.
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
challenge_method_details | 1 | Return Tempo-specific fields (chainId, feePayer, memo, presenterBinding) for the 402 challenge. | charge: value |
verify | 2 | Verify a Tempo credential by checking on-chain settlement. | payload: value, charge: value |
validate_config! | 1 | Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies. | config: value |
method_name | 0 | Return the payment method identifier for Tempo. | - |
Summary
Functions
Return Tempo-specific fields (chainId, feePayer, memo, presenterBinding) for the 402 challenge.
Return the payment method identifier for Tempo.
Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies.
Verify a Tempo credential by checking on-chain settlement.
Functions
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map() | nil
@spec challenge_method_details(MPP.Intents.Charge.t()) :: map()
Return Tempo-specific fields (chainId, feePayer, memo, presenterBinding) for the 402 challenge.
Parameters
charge- Charge struct with method_details containingchain_id,fee_payer, and optionallymemo(value)
Returns
Map with chainId (default 42431), feePayer (default false), optional memo, and presenterBinding (present and true only when required) (map)
# descripex:contract
%{
params: %{
charge: %{
description: "Charge struct with method_details containing `chain_id`, `fee_payer`, and optionally `memo`",
kind: :value
}
},
returns: %{
type: :map,
description: "Map with `chainId` (default 42431), `feePayer` (default false), optional `memo`, and `presenterBinding` (present and `true` only when required)"
}
}
@spec method_name() :: String.t()
Return the payment method identifier for Tempo.
# descripex:contract
%{}
Validate Tempo method_config at init time. Raises on missing rpc_url or unavailable onchain / onchain_tempo dependencies.
Parameters
config- method_config map to validate (value)
Returns
:ok on success, raises ArgumentError on missing keys (atom)
# descripex:contract
%{
params: %{
config: %{description: "method_config map to validate", kind: :value}
},
returns: %{
type: :atom,
description: "`:ok` on success, raises `ArgumentError` on missing keys"
}
}
@spec verify(map(), MPP.Intents.Charge.t()) :: {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) :: {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) :: {:ok, MPP.Receipt.t()} | {:error, MPP.Errors.t()}
@spec verify(map(), MPP.Intents.Charge.t()) :: {:error, MPP.Errors.t()}
Verify a Tempo credential by checking on-chain settlement.
Parameters
payload- Credential payload map with"type"("hash"or"transaction") and corresponding proof field (value)charge- Charge intent struct with amount, currency, and method_details (includingrpc_url) (value)
Returns
{:ok, receipt} on success, {:error, error} on failure (tagged_tuple)
Errors
:invalid_payload:verification_failed
# descripex:contract
%{
params: %{
payload: %{
description: "Credential payload map with `\"type\"` (`\"hash\"` or `\"transaction\"`) and corresponding proof field",
kind: :value
},
charge: %{
description: "Charge intent struct with amount, currency, and method_details (including `rpc_url`)",
kind: :value
}
},
errors: [:invalid_payload, :verification_failed],
returns: %{
type: :tagged_tuple,
description: "`{:ok, receipt}` on success, `{:error, error}` on failure"
}
}