Skip to content
Integra Protocol

Channels, sessions and subscriptions

One ATR for a whole channel, session or subscription, bound where it opens.

Some payments are not one-off. An x402 batch-settlement channel takes a deposit and then pays each request with a voucher. An MPP session does the same with a payment channel, and an MPP subscription activates once and renews each period. Paying request by request under a fresh ATR each time would make every voucher an agreement of its own.

These pairings bind one ATR to the whole channel, session or subscription. H rides where the channel opens: in its salt, its memo, or the authorization that activates it. The later payments within it carry no ATR of their own. Another agreement opens another channel.

sequenceDiagram
  participant B as Buyer
  participant S as Seller
  S-->>B: challenge carrying H and the link
  Note over B: buyer gate: fetch, compare with H, once
  B->>S: the opening, with H in its salt, memo or witness
  Note over S: bound(opening) equals the H it issued, channel.kind is open
  loop each later request
    B->>S: a voucher within the channel
    Note over S: channel.kind is within, channel.ref names the same channel
  end
  B->>S: the close
  Note over S: channel.kind is close

The buyer compares the served bytes with H once, at the opening. After that, a payment within the channel is tied to the ATR through the channel it belongs to.

The pairings

Ten pairings have a channel member. The table says where H rides at the opening, which later payments count as within the channel, and what channel.boundWithin and channel.until read.

PairingH at the openingWithin, and the closeboundWithinuntil
x402/batch-settlement/eip155The channel configuration's salt. The channel id, which the deposit authorization and every voucher sign, commits to it.A voucher, or a refund with an amount, is within; a full refund is the close.The salt of the payment's channel configuration, once it hashes to the voucher's channel id.none
x402/batch-settlement/solanaThe opening transaction's one Memo instruction, H's LCP string (the option's extra.memo).A voucher or a top-up is within; a refund is the close.Refuses x402/not-bound-within.none
mpp/session/evmThe channel's salt: in the signed open call, the EIP-3009 nonce MPP derives over the channel parameters and the salt, or the Permit2 witness.A voucher or a top-up is within; close is the close.Refuses mpp/not-bound-within.none
mpp/session/tempoThe salt of the signed open call to the channel escrow.A voucher or a top-up is within; close is the close.On the v2 escrow, the salt in the payment's channel descriptor, once the descriptor's channel id is the payment's channel.none
mpp/session/hederaThe salt of the escrow's open.A voucher, a top-up or a use is within; close is the close.Refuses mpp/not-bound-within.none
mpp/session/solanaThe open instruction's salt: H's first 8 bytes.A voucher, a top-up or a use is within; close is the close.H from a use credential whose session proof names the opening challenge's id.none
mpp/session/xrplThe PaymentChannelCreate's one memo, H's LCP string.A voucher is within; close is the close.Refuses mpp/not-bound-within.The opening's CancelAfter, where it sets one.
mpp/session/lightningThe deposit invoice's description hash h, which the seller's node signs.A bearer proof or a top-up is within; close is the close.Refuses mpp/not-bound-within.none
mpp/subscription/tempoThe witness of the key authorization the payer's root key signs.Only the activation, a key authorization, is classified: it is the opening.Refuses mpp/not-bound-within.The request's subscriptionExpires.
mpp/subscription/stripeThe request's methodDetails.metadata.legal_context.Every payment the seller reports on this pairing is the activation.Refuses mpp/not-bound-within.none

This example lists them from the registry:

import {  } from "@integraledger/lcp";

const  = .(() => "channel" in ).(() => .);
.(.);
.(.().("\n"));
10
mpp/session/evm
mpp/session/hedera
mpp/session/lightning
mpp/session/solana
mpp/session/tempo
mpp/session/xrpl
mpp/subscription/stripe
mpp/subscription/tempo
x402/batch-settlement/eip155
x402/batch-settlement/solana

Two x402 pairings carry a request-scoped form of the same idea, with no channel member: x402/batch-settlement/cloudflare echoes H in the legalContext extension of each request, and x402/upto/solana opens a one-request payment channel whose opening transaction carries H as its memo.

The LCP profiles x402/batch-settlement/eip155, mpp/session/evm-tempo and mpp/session/hedera-solana-xrpl state the rules for these bindings.

The channel members

On top of the members every pairing has, a channel pairing has:

MemberSideWhat it does
channel.kind(payment)seller"open", "within" or "close", from the payment's own action or type, or a refusal.
channel.ref(payment)sellerThe channel's key: { network, channel }. The opening and every payment within share it.
channel.boundWithin(payment)sellerH from a payment within the channel, where the pairing's payments within carry it; otherwise a refusal.
channel.until(payment)sellerThe channel's end in Unix seconds, where its opening sets one.
buildWithin(within, h)buyerA later voucher, or the close, for a channel opened under H. It refuses a channel that was not.
closeRef(challenge, channel)sellerThe read keys of the channel's close, on the MPP sessions and on mpp/subscription/tempo.

buildWithin exists on the two x402 channels and the MPP sessions on EVM, Tempo, Hedera, Solana and the XRP Ledger. On MPP it builds a voucher or the close. It does not build a top-up, which is a further deposit, or Solana's operator use.

mpp/subscription/stripe's channel.ref takes the payment's receipt as a second argument: the Stripe subscription it names is the channel.

One channel, end to end

This program opens an x402 batch-settlement channel on an EVM chain, pays one voucher within it, and closes it. Both sides run in one process: a random key stands in for the buyer's signer and no network is used. Signing uses viem.

import { , ,  } from "@integraledger/lcp";
import { , , type , type  } from "@integraledger/lcp/x402";
import { , type  } from "@integraledger/lcp/x402-batch-settlement";
import type {  } from "viem";
import { ,  } from "viem/accounts";

const  = (());
async function (: readonly []): <string[]> {
  const : string[] = [];
  for (const  of ) {
    if (. !== "eip712") throw new (`this signer signs EIP-712 only, not ${.}`);
    .(await .(. as ));
  }
  return ;
}

// Seller: the channel option. 1000 base units of USDC per request, on Base Sepolia.
const :  = {
  : "batch-settlement",
  : "eip155:84532",
  : "1000",
  : "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  : "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
  : 60,
  : {
    : "USDC",
    : "2",
    : "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    : 900,
  },
};
const :  = {
  : 2,
  : { : "https://api.seller.example/v1/stream" },
  : [],
};

// Seller: one ATR for the whole channel, and H advertised in the challenge.
const  = await ({ : "GET", : "/v1/stream", : new () });
if ("refused" in ) throw new (.);
const  = new ().('{"text":"1000 base units of USDC per request."}');
const  = await ((), (., ), [["terms", ]]);
if ("refused" in ) throw new (.);
const  = .(, ., `https://atr.seller.example/${.}`, );
if ("refused" in ) throw new (.);

// Buyer: read H, compare the served bytes with it as the buyer guide shows, then open the channel with H as its salt.
const  = .();
if ("refused" in ) throw new (.);
const  = ..[0]!;
const  = await .(
  {
    : ,
    ,
    : .,
    : .,
    : 100_000n,
    : `0x${"01".(32)}`,
    : .(.() / 1000),
  },
  .,
);
if ("refused" in ) throw new (.);
const  = ..(() => (. === "eip712" ? .. : .));
.("the opening signs:", );
const  = .(await (.));
if ("refused" in ) throw new (.);

// Seller: the opening is bound to the H it issued, and names the channel.
const  = await .();
const  = await ..();
if ("refused" in ) throw new (.);
.(..(), "bound to H:", typeof  === "string" && (, .));

// Buyer: a later voucher in the same channel, for a cumulative 2000 base units.
const  = .["channelConfig"]!;
const  = await .(
  { : , , , : 2000n },
  .,
);
if ("refused" in ) throw new (.);
const  = .(await (.));
if ("refused" in ) throw new (.);

// Seller: the voucher is within the same channel, and its configuration still carries H.
const  = await ..();
const  = await ..();
const  = !("refused" in ) && . === .;
.(..(), "same channel:", );
.("within bound to H:", typeof  === "string" && (, .));
const  = await .();
.("bound(voucher):", typeof  === "string" ?  : .);

// Buyer: the close, a full refund of what is left.
const  = await .(
  { : , , , : 2000n, : {} },
  .,
);
if ("refused" in ) throw new (.);
const  = .(await (.));
if ("refused" in ) throw new (.);
.(..());
the opening signs: [ 'ReceiveWithAuthorization', 'Voucher' ]
open bound to H: true
within same channel: true
within bound to H: true
bound(voucher): x402/not-an-opening
close

Keep the opening exactly as the buyer signed it: every payment within the channel is built from its channel configuration, and buildWithin refuses a configuration whose salt is not H (x402/channel-id-mismatch).

What each side keeps

The seller refuses an opening whose H it did not issue for that request, or has already seen claimed. After that, it matches each later payment to an open channel by channel.ref, and so to that channel's ATR. What a voucher is worth, and when to settle, are the seller's to decide under the scheme. The channel members classify a payment and name its channel; none of them reads a voucher's amount.

The buyer keeps the ATR's bytes it compared at the opening, and the opening itself. On MPP, a session challenge that names a channelId resumes a channel (evmSessionResume reads it on EVM and Tempo): the buyer pays it with a voucher only for a channel it opened for an ATR it compared.

What the record says

Each channel pairing's pattern.proves states what the opening shows, and then that the later payments in the channel, session or subscription were made under the same ATR. Some also say what those payments sign: on the EVM, Tempo and Hedera sessions and the EVM batch-settlement channel, each voucher signs a commitment to H; on the XRP Ledger session, each voucher signs the channel id and an amount, not H. The pairings reference quotes each one.

Reading settlement

reference(opening) and status(ref, reader) read the opening's settlement, as on any pairing. On the MPP sessions and mpp/subscription/tempo, closeRef(challenge, channel) gives the read keys of the close from the issued challenge and the channel, and status reads the close through the same reader. On mpp/session/evm, a transaction that is not a call closing that channel reads as pending, with the reason not-a-close.

Next

  • x402 and MPP: the surfaces these channels run on.
  • Buyer: the comparison the buyer makes before the opening.
Edit on GitHub

Last updated on

On this page