LOPWDocumentation

Docs / Networks

EVM binding

One Solidity binding serves every EVM network. It is built against Base first (Base Sepolia for the test network) and runs unchanged on Ethereum, Arbitrum, Optimism, Polygon and Avalanche with each network’s own USDC and Chainlink addresses. Semantics are those of the protocol specification; only the carriers differ from Stellar.

Shared by six networks Base first

At a glance

AspectEVM binding
ContractsSolidity (Foundry): Bond Registry, Claims, six validators; admin is Ownable2Step; reentrancy guard on every token-moving function; USDC moves with SafeERC20
Signaturessecp256k1 over EIP-712 typed data; the service signing key is an ordinary key held by the seller’s server and registered as an address, never the seller’s wallet
Hashingkeccak-256; offerHash and receiptHash are EIP-712 digests (_hashTypedDataV4(structHash)); termsHash = keccak256(abi.encode(terms))
Stake tokenNative USDC (ERC-20, 6 decimals); the 100 USDC floor is 100_000_000 units
P6 oracleChainlink AggregatorV3Interface.getRoundData(roundId); the cited round id plays the role of Stellar’s oracle timestamp
x402 schemeexact on eip155:<chainId> with USDC transferWithAuthorization (EIP-3009)
Wire formabi.encode(SignedOffer) and abi.encode(SignedReceipt) as 0x-prefixed hex inside extensions.lopw
Indexingeth_getLogs on the registry and claims addresses from the deployment block, after EVM_CONFIRMATIONS confirmations (default 12 on Base Sepolia)

Types

Solidity structs; field order is normative because it defines the ABI and the EIP-712 hashes. Field names are the Stellar names in camelCase.

Solidity
struct OracleTerms { address feed; bytes32 asset; uint32 maxDeviationBps; uint64 maxOracleAge; }

struct Terms {
    uint8[]  predicates;   // 1..6, strictly increasing, no duplicates
    uint64   claimWindow;
    uint64   maxDataAge;
    uint16[] allowedStatus;
    uint32   maxResponseBytes;
    bool     hasOracle;
    OracleTerms oracle;    // zeroed when hasOracle is false
}

struct Offer {
    uint32  version;       // 1
    bytes32 serviceId;
    address registry;
    bytes32 termsHash;
    uint256 price;
    address asset;         // USDC
    uint256 maxClaim;
    uint64  validUntil;
    bytes32 offerNonce;
    uint32  keyVersion;
}

struct OracleEvidence { address feed; bytes32 asset; uint80 roundId; int256 oraclePrice; int256 reportedValue; }

struct Receipt {
    uint32  version;
    bytes32 serviceId;
    bytes32 offerHash;
    bytes32 termsHash;
    address buyer;
    bytes32 paymentReference;  // settlement transaction hash
    bytes32 requestHash;
    bytes32 responseHash;
    uint32  responseBytes;
    uint16  status;
    uint64  servedAt;
    uint64  dataAsOf;
    bool    hasOracle;
    OracleEvidence oracle;     // zeroed when hasOracle is false
    uint32  keyVersion;
}

struct SignedOffer   { Offer offer;     bytes signature; }   // 65-byte secp256k1 (r, s, v)
struct SignedReceipt { Receipt receipt; bytes signature; }
struct ClaimExtra    { bool hasSecond; SignedReceipt second; }   // P1 only

termsHash = keccak256(abi.encode(terms)). Service ids are keccak256(abi.encode(address(registry), uint256(counter))) with the counter starting at 0 per registry. requestHash and responseHash are the same SHA-256 constructions as on Stellar, so a seller’s HTTP layer is chain-agnostic.

Hashing and signing (EIP-712)

Domain: EIP712Domain(name="LOPW", version="1", chainId, verifyingContract=registry). The chain id and the registry address give the same two replay protections as Stellar’s network id and registry address in the hash.

EIP-712 type strings
Offer(uint32 version,bytes32 serviceId,address registry,bytes32 termsHash,uint256 price,address asset,uint256 maxClaim,uint64 validUntil,bytes32 offerNonce,uint32 keyVersion)
OracleEvidence(address feed,bytes32 asset,uint80 roundId,int256 oraclePrice,int256 reportedValue)
Receipt(uint32 version,bytes32 serviceId,bytes32 offerHash,bytes32 termsHash,address buyer,bytes32 paymentReference,bytes32 requestHash,bytes32 responseHash,uint32 responseBytes,uint16 status,uint64 servedAt,uint64 dataAsOf,bool hasOracle,OracleEvidence oracle,uint32 keyVersion)

Contracts

Functions keep the Stellar names where possible; errors are custom errors with the Stellar names; events carry the same names and fields in CamelCase, so one indexer schema serves every chain.

Solidity interfaces
// Bond Registry
constructor(address admin, IERC20 usdc, ProtocolConfig config, LaunchLimits limits, FeeConfig fees)
setClaimsContract(address) -> uint64 eta          // first call immediate; later ones time-locked
applyClaimsContract(); cancelClaimsChange()
setLimits(LaunchLimits) -> uint64 eta              // tightening immediate, loosening time-locked
setPaused(bool); setSellerAllowed(address, bool); setFees(FeeConfig) -> uint64 eta
registerService(address signingKey, Terms terms, uint256 minStake) -> bytes32 serviceId
updateTerms(bytes32, Terms) -> bytes32 termsHash;  rotateKey(bytes32, address) -> uint32 keyVersion
deposit(bytes32, uint256)                          // seller must have approved USDC
requestUnbond(bytes32, uint256); cancelUnbond(bytes32); withdraw(bytes32) -> uint256; collectFee(bytes32) -> uint256
slash(bytes32, address buyer, uint256 amount) -> uint256   // onlyClaims
getService / getTerms / getKey / termsHash / getConfig / getLimits / getFees / isPaused / isSellerAllowed / getTotalStake

// Claims
constructor(address admin, address registry, uint64 changeDelay)
setValidator(uint8 predicateId, address) -> uint64 eta; applyValidator(uint8); cancelValidatorChange(uint8)
claim(uint8 predicateId, SignedOffer, SignedReceipt, ClaimExtra) -> (bool paid, uint256 amount, uint8 reason)
getClaim(bytes32 serviceId, bytes32 paymentReference) -> ClaimRecord

// Validators
validate(Terms, Offer, Receipt, ClaimExtra) external view returns (uint8)   // 0 proven, 1 NotProven, 2 NotVerifiable

The claim runs the seven checks in the specification’s order and reverts with the Stellar error names (UnknownService, BadSignature, OfferMismatch, WindowClosed, AlreadyClaimed, PredicateNotOffered, NoStake, ValidatorNotSet, KeyRetired, UnsupportedVersion, SlashRateLimited). The window check uses block.timestamp. Outcomes are ClaimPaid(serviceId, predicateId, buyer, amount, paymentReference) and ClaimRejected(serviceId, predicateId, reason, paymentReference) with reason 1 = NotProven and 2 = NotVerifiable. The P1 second receipt is signature-checked by Claims before the validator runs. Fees accrue per second with SECONDS_PER_YEAR = 31_536_000 and basis points over 10,000.

P6 with Chainlink

  1. receipt.oracle.feed == terms.oracle.feed && receipt.oracle.asset == terms.oracle.asset, otherwise proven (terms mismatch).
  2. (, int256 answer,, uint256 updatedAt,) = feed.getRoundData(receipt.oracle.roundId); a revert, answer <= 0 or updatedAt == 0 is NotVerifiable.
  3. updatedAt > servedAt or servedAt − updatedAt > maxOracleAge: proven (stale).
  4. |reportedValue − answer| × 10000 > maxDeviationBps × answer: proven, exact integer arithmetic (decision D-009).

Chainlink rounds are immutable once published, so a cited roundId is the citation, exactly as the oracle timestamp is on Stellar. Terms name the feed by its pair label (asset = bytes32("ETH/USD")) and the reference service reports values in the feed’s decimals (8 on Chainlink USD feeds, against 14 on Reflector); the deviation check is decimal-agnostic because both sides come from the same feed.

x402 on EVM

Scheme exact on eip155:<chainId> with USDC transferWithAuthorization (EIP-3009). The seller’s PaymentRequirements.extra carries the asset’s EIP-712 name and version as x402 requires ({ "name": "USDC", "version": "2" } on Base Sepolia, verified on-chain). paymentReference is the settlement transaction hash from PAYMENT-RESPONSE.transaction; buyer is the payer address. The lopw extension is carried exactly as on Stellar; only the signed-offer and signed-receipt encoding changes.

402 PaymentRequired, EVM
"extensions": {
  "lopw": {
    "info": {
      "version": 1,
      "network": "base-sepolia",
      "registry": "0x…",                    // Bond Registry contract address
      "service_id": "…64 hex…",
      "offer": "0x0000…"                    // abi.encode(SignedOffer { offer, signature }) as hex
    },
    "schema": { "type": "object" }
  }
}
// PAYMENT-RESPONSE.extensions.lopw.receipt: abi.encode(SignedReceipt { receipt, signature }) as 0x-hex

SDK

TypeScript
import { EvmBinding, EVM_NETWORKS } from "@lopw/binding-evm";

const binding = new EvmBinding(EVM_NETWORKS["base-sepolia"]);   // chain id 84532, USDC and feed addresses included
// EVM_RPC_URL overrides the public JSON-RPC; LOPW_ORACLE overrides the feed; registry and claims come from deployments/<name>.json
EvmNetwork fieldMeaning
nameNetwork name used in API paths, for example base-sepolia
caip2eip155:<chainId>, used by x402
chainIdThe EIP-712 domain chain id
rpcUrlPublic JSON-RPC used by default
usdc, usdcDecimalsCircle’s native USDC contract, 6 decimals; checked on-chain (symbol(), decimals()) before use
registry, claimsLOPW contract addresses, filled from deployments/<name>.json once a deployment is published
oracleThe Chainlink feed the reference service measures against, verified on-chain (description(), decimals())
explorerTx, explorerContractExplorer URL builders for the network

Networks on this binding

ChainTest networkChain idProduction networkChain idLOPW deployment
Basebase-sepolia84532base8453None published
Ethereumsepolia11155111ethereum1None published
Arbitrumarbitrum-sepolia421614arbitrum42161None published
Optimismoptimism-sepolia11155420optimism10None published
Polygonpolygon-amoy80002polygon137None published
Avalancheavalanche-fuji43113avalanche43114None published

For agents

For agents

ItemValue
Network name in API pathsbase-sepolia, base, sepolia, ethereum, arbitrum-sepolia, arbitrum, optimism-sepolia, optimism, polygon-amoy, polygon, avalanche-fuji, avalanche; a network is served once a deployment is published for it
CAIP-2 ideip155:<chainId> (the chain id is also the EIP-712 domain chain id)
Wire encoding of extensions.lopwinfo.offer = abi.encode(SignedOffer) and receipt = abi.encode(SignedReceipt), 0x-prefixed lowercase hex; JSON Schemas offer-evm.json, receipt-evm.json, terms-evm.json
Signature65 bytes (r, s, v) over the EIP-712 digest; recover and compare with getKey(serviceId, keyVersion)
AmountsDecimal strings of USDC base units, 6 decimals