LP-182 defines a single, transport-portable protocol for AI agents to discover, negotiate, and settle payments — for individual API calls, for streaming work over time, or for multi-party flows where N agents split a single inbound payment. The same PaymentOffer / PaymentProof data model serves four transport layers:
1. HTTP/402 — discovery. Compatible with the x402 spec; any HTTP route can demand payment by returning an accepts[] envelope and accepting an X-PAYMENT header on retry.
2. ZAP wire (LP-022) — high-volume A2A. Zero-allocation binary frames for agents on the same Lux chain exchanging offers and proofs at line rate.
3. On-chain escrow — settlement. A trustless AgentEscrow contract with timeout, challenge period, and mutual cancel.
4. Streaming sessions (LP-047) — continuous flow. Per-second token streams for long-running work, vesting, or subscription flows, with mid-stream upgrade to multi-party (MPP) sessions.
Compliance, identity, and threshold-signing slot in via existing Lux primitives — LP-088 (compliance hooks), LP-001 (security tokens), LP-019 (threshold MPC). Replay protection, expiry, and currency selection are normative parts of the wire format. The reference implementation is kcolbchain/switchboard.
This document is a refiling of the agent-payments draft originally submitted as LP-167 in luxfi/LPs#655, which was closed on 2026-05-24 after the LP-167 slot was reallocated to the FHE Runtime Final (see LP-167-lux-fhe-runtime.md) and LP-181 was taken by Magnetar (LP-181-magnetar-threshold-slhdsa.md). The technical content is unchanged from the original draft except where explicitly noted; only the LP number and shipped-status fields have moved.
Every AI agent that wants to call a paid API or hire another agent today rolls its own payment plumbing: bespoke JSON over HTTP, Stripe meters, custom RPC paywalls, ad-hoc ETH transfers. That works for one app. It collapses the moment two agents from different teams need to settle on the fly.
Three rails exist in the wild but none compose:
LP-182 is the union: a single PaymentOffer data type that any of the four transports can carry, on top of Lux's existing crypto and securities primitives. Agents that learn the data model speak HTTP, ZAP, escrow, and streams without code branches.
accepts[] envelope is byte-for-byte compatible with x402's accepts[]. An LP-182 server can be queried by any x402 client. |WWW-Authenticate: x402 and X-PAYMENT headers are normative. |PaymentOffer and PaymentProof are signed via EIP-712 typed-data structures (canonical domain separator below). |ComplianceRegistry before settlement. |0x70 PaymentOffer, 0x71 PaymentProof). |Stream (LP-047) plus an OfferRef linking back to the originating quote. |An agent identity is a 3-tuple:
AgentID := (chainID, address, optional DID)
chainID | uint64 | Lux L1 ID, EVM chain ID, or the special value 0 for off-chain agents identified solely by key |address | [20]byte | EVM address derived from the agent's signing key |did | optional string | If present, MUST resolve via LP-060 (DID Specification) |AgentID is canonicalized for hashing as keccak256(chainID || address || did) with did length-prefixed (u32 BE).
A PaymentOffer is an unsigned commitment by a payee to accept a specific payment in exchange for a specific resource.
PaymentOffer {
scheme: u8 // 0=EXACT, 1=STREAMING, 2=MPP, 3-255 reserved
chain_id: u64 // settlement chain
expires_at: u64 // unix seconds; 0 = no expiry
recipient: [20]byte // payee address
amount: [32]byte // uint256 big-endian, in asset's smallest unit
currency: string // ERC-20 symbol or "LUX" or "ETH"
asset: [20]byte // ERC-20 contract; zero for native
description: string // human-readable
endpoint: string // resource the offer covers (URL, contract method, etc.)
nonce: string // payee-chosen, MUST be unique per offer
}
All variable-length strings use UTF-8 with a u32 BE length prefix on the wire.
struct PaymentOffer {
uint8 scheme;
uint64 chainId;
uint64 expiresAt;
address recipient;
uint256 amount;
string currency;
address asset;
string description;
string endpoint;
string nonce;
}
PaymentOfferDigest is keccak256(EIP-712Encode(domain, PaymentOffer)) where the EIP-712 domain is:
{
name: "Lux Agent Payments",
version: "1",
chainId: <PaymentOffer.chain_id>,
verifyingContract: <AgentEscrow address>
}
0x70)Frame layout matches LP-022 §"Frame Format". Payload is fixed-offset:
+---------+---------+-----------+-------------+-------------+
| scheme | chain_id| expires | recipient | amount |
| 1B | 8B BE | 8B BE | 20B | 32B BE |
+---------+---------+-----------+-------------+-------------+
| asset | currency_len | currency | description_len |
| 20B | 4B BE | UTF-8 var | 4B BE |
+---------+--------------+---------------+------------------+
| description | endpoint_len | endpoint | nonce_len | nonce|
| UTF-8 var | 4B BE | UTF-8 var | 4B BE | UTF-8|
+-------------+--------------+-----------+-----------+------+
expires_at == 0 is the canonical "no expiry" sentinel. currency, description, endpoint, nonce MAY be empty (length 0) but the prefix MUST always be present.
A signed offer on the wire is { offer_bytes || signature[65] } where the signature is the EIP-712 sig of PaymentOfferDigest.
A PaymentProof is a signed receipt produced by the payer after settlement.
PaymentProof {
chain_id: u64
timestamp: u64 // unix seconds
payer: [20]byte
tx_hash: [32]byte // settlement tx (or escrow create tx for escrow scheme)
amount: [32]byte // uint256 big-endian, MUST match offer
nonce: string // MUST match offer.nonce
}
ZAP wire encoding uses message type 0x71. EIP-712 type:
struct PaymentProof {
uint64 chainId;
uint64 timestamp;
address payer;
bytes32 txHash;
uint256 amount;
string nonce;
}
A server gates a route by returning HTTP 402 with the body:
{
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "lux-c",
"maxAmountRequired": "1000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x...",
"resource": "https://example/v1/infer",
"description": "model inference, 1k tokens",
"mimeType": "application/json",
"expiresAt": 1714521600
}
]
}
Headers:
WWW-Authenticate: x402 | response | yes | discoverability for non-LP-182 clients |x402-version | response | yes | spec version, currently 1 |Accept-Payment | request | optional | scheme preference (exact, streaming, mpp) |X-PAYMENT | request | yes (on retry) | base64-encoded PaymentProof for exact, or session token for streaming/mpp |A client that already holds a valid PaymentProof MAY pre-emptively include X-PAYMENT on the first request and skip the 402 round-trip. Servers MUST accept this.
Single-shot quote-and-settle. The payer:
1. Receives 402 with accepts[].
2. Picks an accept entry, transfers amount of asset to payTo on network.
3. Signs a PaymentProof referencing the tx hash.
4. Retries the request with X-PAYMENT: base64(proof).
The server verifies (a) the on-chain transfer, (b) the proof signature matches the payer of that transfer, (c) nonce matches the offer it issued, (d) expiresAt has not passed.
Long-running work. Constructed as an LP-047 Stream plus an OfferRef:
struct OfferRef {
bytes32 offerDigest; // PaymentOfferDigest
address payee;
address payer;
uint256 streamId; // LP-047 stream
}
The payer creates an LP-047 stream from itself to the payee for the duration covering the work. The session token presented in X-PAYMENT is keccak256(offerDigest || streamId). The server verifies the stream is live (balance(payee) > 0), un-canceled, and points back to its issued offer.
Stream cancellation by the payer terminates the agent's session. The server SHOULD return 402 with a fresh offer to re-up.
A single inbound payment to a coordinator splits across N agents that contributed. Each agent presents a co-signed PaymentOffer whose amount is the agent's share and whose recipient is a threshold address (LP-019 / LP-076). The coordinator settles once on-chain to the threshold address; the threshold quorum signs a release transaction splitting per the agents' shares.
PaymentOffer.scheme = MPP
PaymentOffer.recipient = threshold_address (controlled by quorum of N agents)
PaymentOffer.description = "session=<id>;split=alice:0.4,bob:0.4,carol:0.2"
Replay protection is via the nonce field plus the on-chain quorum's monotonically increasing sessionCounter.
A reference contract (AgentEscrow.sol) provides trustless settlement when neither side trusts the other. Spec excerpt:
interface IAgentEscrow {
function createPayment(
bytes32 offerDigest,
address payee,
uint256 amount,
address asset, // 0x0 for native LUX/ETH
uint64 timeoutBlocks,
uint64 challengePeriodBlocks
) external payable returns (bytes32 requestId);
function confirmPayment(bytes32 requestId) external; // payer releases
function requestRefund(bytes32 requestId) external; // after timeout
function cancelPayment(bytes32 requestId) external; // mutual
event PaymentCreated(bytes32 indexed requestId, address indexed payer, address indexed payee, uint256 amount, bytes32 offerDigest);
event PaymentConfirmed(bytes32 indexed requestId);
event PaymentRefunded(bytes32 indexed requestId);
}
offerDigest MUST be PaymentOfferDigest of the original signed offer. Indexers MAY join PaymentCreated to off-chain offers via that digest.
When asset is a security token (ERC-3643, LP-088), the AgentEscrow.createPayment and AgentEscrow.confirmPayment calls MUST consult ComplianceRegistry.canTransfer(payer, payee, amount) before holding or releasing funds. A non-zero restriction code reverts with the canonical error message from LP-088. Agent identities that hold security tokens MUST be registered in the IdentityRegistry.
This is the bridge that lets a regulated workflow (security-token-denominated agent work) execute on the same protocol as commodity USDC payments.
For MPP and high-value flows, a PaymentOffer MAY be signed by a quorum of agent keys via LP-019 / LP-155 (CGGMP21 ECDSA) or LP-154 (FROST Schnorr). The signature on the wire is replaced with the threshold signature (still 65 bytes for ECDSA; 64 + 1 recovery id for Schnorr). Verification follows the curve specified in PaymentOffer.scheme extension byte (reserved for future use); for v1, ECDSA is implied.
The threshold quorum SHOULD be defined by an on-chain registry (per-session) so a verifier can resolve which keys participated.
PaymentOffer.nonce | payee-chosen UUIDv4 or monotonic counter, scoped to (payee, endpoint) |PaymentOffer.expires_at | hard expiry |PaymentProof.tx_hash | settlement uniqueness — payer cannot reuse a proof against multiple offers |AgentEscrow.requestId | derived from keccak256(payer, payee, nonce, block.timestamp) |A server MUST reject any PaymentProof whose nonce it has already recorded as redeemed. A PaymentProof is one-shot.
When PaymentOffer.chain_id differs from the chain hosting the AgentEscrow, settlement MAY use LP-016 (OmnichainRouter) or LP-017 (Native Bridge Programs for Teleport). The proof's tx_hash MUST reference the destination-chain settlement; the server validates via Warp messaging (LP-021) or a light-client query.
PaymentOffer.currency is informative; PaymentOffer.asset is normative. Servers SHOULD list multiple accepts[] entries when willing to take more than one asset (e.g., USDC on Lux-C, USDT on Lux-C, native LUX). Clients pick the cheapest fit.
asset == 0x0 is the canonical sentinel for native chain currency.
The four-transport / one-data-model split is the load-bearing design choice. Several alternatives were considered and rejected:
1. Pure HTTP/402 (x402 only). Simplest, but cannot settle multi-party flows without out-of-band coordination, cannot stream, and does not expose escrow semantics for distrustful counterparties.
2. Pure on-chain (one escrow contract). Trustless but slow and expensive for high-volume per-call payments; also requires every agent to be on-chain by default.
3. Per-transport bespoke schemas. What the ecosystem has today — a payer that wants to be reachable over HTTP, ZAP, escrow, and streams ends up implementing four code paths and four signature schemes. LP-182 collapses these to one EIP-712 typed-data shape.
The choice of EIP-712 (over a custom signing scheme or simple personal_sign) is to keep the signed digest meaningful in wallet UIs and compatible with the existing Ethereum tooling chain; agents that need to settle on non-EVM chains can still parse the digest deterministically because the EIP-712 encoding is fully specified.
ZAP message types 0x70 and 0x71 are chosen from the LP-022 reserved application range. They are fixed-offset to allow zero-allocation parsing in hot agent paths.
LP-182 v1 is additive:
accepts[] envelope is wire-compatible).The new ZAP message types (0x70, 0x71) are reserved by this LP and require no protocol-level changes to LP-022.
Replacement of the closed LP-167 draft (PR #655) is non-breaking — no implementations had shipped against the original LP-167 number, and the reference implementation (kcolbchain/switchboard) used the data model directly, not the LP number, as its versioning anchor.
Reference test vectors live in kcolbchain/switchboard/tests/fixtures/zap_wire/:
offer_minimal.bin | EXACT scheme, no expiry, empty optional strings |offer_full.bin | STREAMING scheme, uint256-max amount, USDC on Lux-C, full description and endpoint |proof_canonical.bin | Production-shape PaymentProof for the matching offer |Each vector includes the matching JSON shape and the EIP-712 PaymentOfferDigest so any third-party implementation can verify byte-equivalence.
kcolbchain/switchboard — Python + Solidity, MIT.
switchboard/x402_middleware.py — server-side HTTP/402 middleware, FastAPI/Flask drop-in.switchboard/zap_transport.py — PaymentOffer / PaymentProof ZAP wire encode/decode (uses luxfi/zap zap_py).switchboard/gas_tracker.py + gas_budget.py — per-hour / per-day client-side spend caps.switchboard/nonce_manager.py — client-side nonce manager with reorg protection.contracts/AgentEscrow.sol + src/payment_protocol.py — on-chain escrow + Python client.web/ — public side-by-side explorer for x402 / MPP / AP2 / Circle / on-chain escrow.The implementation has been audited internally; an external audit is expected during the LP-182 Final review phase.
Mitigated by nonce, tx_hash, expires_at, and the server-side proof-redemption ledger (§9). A compromised payee key can sign duplicate offers but cannot redeem the same proof twice against a compliant server.
A passive observer of an unsigned PaymentOffer cannot front-run settlement because the proof ties to the payer's address. A passive observer of an in-flight PaymentProof cannot replay because the server records nonce redemption atomically.
A compromised payer key allows arbitrary spend up to its balance — this is unavoidable for hot-wallet agents. Mitigation: use the gas-budget tracker (switchboard.gas_budget) to cap per-hour and per-day burn before submission, and prefer threshold-signed offers for high-value flows.
An offer with a far-future expires_at is an open option for the payer. Servers SHOULD set short expiries (~5 minutes for EXACT, session-bounded for STREAMING). Clients that hold long-lived offers SHOULD treat them as bearer instruments and protect them accordingly.
Settlement of a security-token offer that skips ComplianceRegistry.canTransfer is a normative violation. Implementations MUST treat compliance failures as a hard error, not a warning.
A subset of MPP participants colluding to claim a larger share than agreed is bounded by the threshold-quorum requirement. The on-chain release transaction MUST be co-signed by the agreed quorum size; a sub-quorum cannot release.
switchboard/x402_middleware.py) | shipped | 2026-04-29 |Final | after audit + 2 independent implementations | Q4 2026 |Copyright and related rights waived via CC0.