AI Infrastructure

    AMP Deep Dive: Channel State, Settlement, and Security

    12 min read
    Aggelos Kappos(Founder @ QBT Labs)
    AMPPayment ChannelsCardanoSolanaAI AgentsSecurityAiken
    AMP Deep Dive: Channel State, Settlement, and Security
    Share:

    The previous post covered AMP from a product perspective — what it is, why it matters, and how to use it. This post goes deeper: how does channel state actually work on different chains, what makes the settlement mechanism secure, and what are the real attack vectors you need to think about when building on top of persistent payment channels?

    This is for builders. If you're integrating AMP into your service or building on top of the protocol, you need to understand this.


    The Core State Machine

    An AMP channel has a simple lifecycle:

    Open → Active → [Settled (repeating)] → Closed
    

    But "simple" at the state machine level hides significant complexity in the implementation. Let's go through each transition.

    Open

    Opening a channel requires:

    1. Both parties knowing each other's public keys (funder and recipient)
    2. The funder depositing funds on-chain into a vault controlled by the channel contract/script
    3. Channel parameters being recorded on-chain: settle_interval, rate_limit, token, nonce

    On Cardano, this means:

    • Minting the Channel NFT (proves ownership, provides stable ID)
    • Creating the script UTxO with the initial ChannelDatum
    • Locking ADA (or native token) in the script address

    The channel is identified by the NFT's asset ID, not the UTxO reference. This is crucial — every settlement produces a new UTxO, so you can't use the UTxO reference as a stable ID.

    On Solana, the channel is a PDA derived from:

    Seeds: [b"amp-channel", funder_pubkey, recipient_pubkey, nonce_le_bytes]
    

    This PDA is the stable channel identifier across the lifecycle.

    Active

    In the Active state, the funder sends requests with:

    AMP-Channel: <stable channel ID>
    AMP-Seq:     <monotonically increasing integer>
    AMP-Sig:     <ed25519(seq as LE u64, funder_private_key)>
    

    The server maintains off-chain state per channel:

    interface ChannelMeteringState {
      channelId: string
      lastSeq: number           // replay prevention
      callCount: number         // for MeteringProof
      totalAmount: bigint       // accumulated owed amount
      periodStart: number       // unix timestamp
      seqStart: number          // for audit trail
    }
    

    This state lives in server memory (persisted to disk for restart recovery). It is the server's claim against the channel. It is not trusted by the on-chain contract — the MeteringProof mechanism is what makes it verifiable.

    Settle

    Settlement is the most security-critical operation. Let's trace exactly what happens.

    Server side:

    1. Compute payload = channel_id || amount || call_count || period_start || period_end || seq_start || seq_end (as bytes)
    2. Compute payload_hash = SHA256(payload) (32 bytes)
    3. Sign: server_signature = Ed25519Sign(recipient_private_key, payload_hash)
    4. Construct MeteringProof (JSON)
    5. Submit settle transaction on-chain with the proof

    On-chain verification (Cardano Aiken):

    Settle { amount, proof } -> {
      // Reconstruct the signed payload
      let payload = build_proof_payload(
        own_input_utxo_ref,
        amount,
        proof.call_count,
        proof.period_start,
        proof.period_end,
        proof.seq_start,
        proof.seq_end
      )
      let payload_hash = sha256(payload)
    
      // Verify signature against recipient's public key
      let sig_valid = builtin.verifyEd25519Signature(
        datum.recipient_pubkey,
        payload_hash,
        proof.server_signature
      )
    
      // All conditions
      sig_valid &&
      amount <= datum.balance &&
      tx_validity_start >= datum.last_settle_ts + datum.settle_interval &&
      channel_nft_in_output(ctx) &&
      output_datum_correct(datum, amount, tx_validity_start)
    }
    

    The key insight: the on-chain contract doesn't trust the server's word. It verifies a cryptographic proof that the server actually signed the claimed usage amounts. A server cannot claim it's owed more than it actually metered, because the client could detect this and dispute the channel.

    Close

    Either party can close. The funder can always close to recover unspent funds. The recipient can close after a timeout if the funder goes silent.

    Close is a final settle + NFT burn:

    Close { final_amount, proof } -> {
      let closer = get_signer(ctx)
      let authorized = closer == datum.funder || closer == datum.recipient
    
      // If final_amount > 0, proof must be valid
      let proof_valid = if final_amount > 0 {
        verify_metering_proof(proof, datum, final_amount)
      } else { True }
    
      // NFT must be burned
      let nft_burned = channel_nft_burned(ctx)
    
      // Remainder goes to funder
      let funder_receives = datum.balance - final_amount
      let funder_paid = value_sent_to(ctx, datum.funder_address) >= funder_receives
    
      authorized && proof_valid && nft_burned && funder_paid
    }
    

    Attack Vectors and Defenses

    1. Replay Attack

    Attack: Attacker captures a valid (AMP-Channel, AMP-Seq, AMP-Sig) tuple from a legitimate request and replays it to get service without consuming their own sequence numbers.

    Defense: The server maintains lastSeq per channel and rejects any request where seq <= lastSeq. Sequence numbers are monotonically increasing and never reused.

    Implementation note: The lastSeq state must survive server restarts. Persist it to disk after every N requests. If the server loses state and accepts a replay, it costs the server (serves a request that's already been paid for), not the client. This is an acceptable failure mode.

    2. Overcharge Attack

    Attack: The server builds a MeteringProof claiming it served 9,000 requests when it actually served 4,500. It signs this proof and submits a settlement for double what's owed.

    Defense: The client tracks its own seq counter. At any point, the client knows it sent seqs 1-4500 in period P. If the server submits a proof for seqs 1-9000, the client can detect the discrepancy and:

    1. Close the channel immediately (unilateral close)
    2. The maximum loss is one unsettled period's worth

    The dispute model in AMP is simple: the funder loses at most one settle_interval period. This is a known, bounded risk. For high-value deployments, set shorter settle intervals (every 10 minutes instead of every hour) to reduce max exposure.

    Note: There is no on-chain dispute resolution mechanism in the base AMP spec. The trust model assumes economic alignment — the server wants repeat business, so overcharging is not in their interest. If you need stronger guarantees, use shorter settle intervals.

    3. Seq Collision / Race Condition

    Attack: Two agents share the same AMP channel (and thus the same private key). They both send requests simultaneously, potentially generating the same seq numbers.

    Defense: Channels are per-funder-key. If you need multiple agents to share a channel budget, use the Delegation feature — the funder sets a delegate key, and the delegate consumes up to their limit. Each agent has their own key, but they share the channel's budget. The server tracks delegate consumption separately.

    4. Channel Squatting

    Attack: Attacker opens channels with a target service but never sends real requests. This wastes the service's resources (storing channel state, querying chain for state).

    Defense: Opening a channel requires a minimum deposit. This deposit is a real cost to the attacker. The service can set minimum deposits high enough to make squatting economically unattractive.

    Additionally, the service can set channel timeouts — if no requests are received within N seconds of channel open, close the channel and return funds.

    5. Stale Channel Data

    Attack: The server caches channel state (balance, status) and doesn't refresh it. A channel that has been closed or drained on-chain still appears valid in the server's cache.

    Defense: The server must refresh channel state:

    • On every settlement (mandatory — needs current state)
    • Periodically (e.g., every 5 minutes) for active channels
    • On any suspicious signal (sudden seq gap, unusual request pattern)

    Channel state can be queried efficiently via Blockfrost (Cardano), Helius (Solana), or Alchemy (Base) — these are indexed APIs, not full node queries.


    The Metering Proof in Detail

    The MeteringProof is the cryptographic bridge between off-chain usage tracking and on-chain settlement. Let's look at exactly what gets signed.

    Signed payload construction:

    function buildProofPayload(proof: MeteringProof): Uint8Array {
      const buf = new ArrayBuffer(32 + 7 * 8) // channel_pda (32) + 7 u64 fields
      const view = new DataView(buf)
    
      // Channel ID (32 bytes — PDA pubkey or NFT policy+name hash)
      const channelBytes = decodeChannelId(proof.channelId)
      new Uint8Array(buf).set(channelBytes, 0)
    
      // 7 fields as little-endian u64
      let offset = 32
      for (const field of [proof.amount, proof.callCount, proof.periodStart,
                           proof.periodEnd, proof.seqStart, proof.seqEnd]) {
        view.setBigUint64(offset, BigInt(field), true) // LE
        offset += 8
      }
    
      return new Uint8Array(buf)
    }
    
    // Total: 32 + 56 = 88 bytes
    // SHA256 of this is what the server signs
    

    Why this specific structure:

    • channel_id: Binds the proof to a specific channel — can't be reused across channels
    • amount: The settlement claim — must match what's transferred on-chain
    • callCount: Auditable — client can verify against their own count
    • periodStart/End: Time-bounds the proof — old proofs can't be replayed
    • seqStart/seqEnd: Auditable — client knows which requests are covered

    Every field serves a purpose. Missing any of them creates an attack vector.


    The QBT Facilitator and AMP

    The QBT Facilitator (facilitator.qbtlabs.io) handles AMP settlements on behalf of services. Instead of the service operator maintaining chain connectivity and signing wallets, they POST the MeteringProof to the facilitator, which:

    1. Verifies the proof signature
    2. Checks the channel state on-chain
    3. Submits the settle transaction
    4. Deducts 0.15% QBT fee
    5. Returns the tx hash

    This means a developer can deploy an AMP-enabled service without running any blockchain infrastructure. The security model is:

    • The facilitator cannot forge proofs (only the service operator's key can sign)
    • The facilitator cannot take more than the proof claims (on-chain validation)
    • The facilitator's fee is transparent and fixed (0.15%)
    • The service operator can always settle directly without the facilitator (self-custody fallback)

    For high-security deployments, self-settlement is always available — just submit the MeteringProof directly from your own wallet.


    Multi-Chain State Comparison

    The same protocol, different on-chain implementations:

    AspectBase (EVM)SolanaCardano
    Channel storageMapping in contractPDA accountScript UTxO + NFT
    Channel IDContract address + channelIdPDA addressNFT asset ID
    Settlementsettle() functionsettle instructionConsume + produce UTxO
    On-chain sig verifyecrecover / Ed25519 precompileEd25519 nativebuiltin.verifyEd25519
    State mutabilityMutable mappingMutable accountImmutable UTxO (consume then produce)
    Concurrent settle riskLow (atomic)Low (atomic)Higher (UTxO contention)

    Cardano-specific: UTxO Contention

    The one area where Cardano requires extra care is concurrent settlement. On EVM and Solana, the channel is a mutable account — two transactions can't both modify it simultaneously (one will fail). On Cardano, both parties could try to consume the channel UTxO at the same time, and both would fail.

    The solution: the server holds an optimistic lock on the channel UTxO when preparing a settlement. If it detects a concurrent modification attempt, it retries after refreshing state. In practice, settlement happens on a timer (e.g., hourly), so collisions are extremely rare.


    Building on @qbtlabs/amp

    The npm package is in development. When it ships, here's what integration looks like:

    For an MCP tool server:

    import { createAMPMiddleware } from '@qbtlabs/amp/server'
    import { MCPServer } from '@modelcontextprotocol/sdk'
    
    const server = new MCPServer({ name: 'my-data-service' })
    server.use(createAMPMiddleware({
      chain: 'cardano',
      pricing: { mode: 'per-call', rate: 500_000n }, // 0.5 ADA per call
      settlement: { interval: 3600 },
      facilitator: { url: 'https://facilitator.qbtlabs.io' }
    }))
    
    server.addTool('get_price', async ({ symbol }, ctx) => {
      // ctx.amp.balance — remaining channel balance
      // ctx.amp.channel — channel identifier
      return { price: await getPriceData(symbol) }
    })
    

    For an AI agent client:

    import { AMPClient } from '@qbtlabs/amp/client'
    
    const amp = new AMPClient({
      chain: 'cardano',
      wallet: loadWalletFromVault('~/.qbt/vault.enc'),
      budget: 50_000_000n, // 50 ADA for this session
    })
    
    // Auto-discovers pricing, opens channel, manages credentials
    const data = await amp.fetch('https://data.example.com/v1/price?symbol=ADA')
    console.log(data.ampBalance) // Remaining budget
    
    await amp.closeAll() // Recover unspent funds
    

    Summary

    AMP is a significant architectural improvement over per-request payments for high-frequency agent workflows. The key properties that make it work:

    1. Channel state on-chain: Funds are locked and verifiable, but state changes don't require constant on-chain transactions
    2. Signed sequence numbers: Prove request authenticity without on-chain verification on every call
    3. Metering proofs: Bridge off-chain usage tracking to on-chain settlement with cryptographic integrity
    4. Ed25519 on-chain verification: Available natively on Cardano (Aiken), Solana (native), and Base (Ed25519 precompile)

    The security model is trust-minimized but not trustless. The server can't forge proofs (key control) and can't take more than earned (on-chain validation). The client's maximum exposure is one settle_interval period. For most agent payment use cases, this is an acceptable trade-off.

    We're building this to be the first production AMP implementation across three chains. Follow progress at github.com/QBT-Labs.


    Related Reading


    Questions? Reach out at [email protected] or find us on X @QBTLabs.

    Related Articles