Skip to content
LinQup Docs
Streaming

How Streaming Works

Technical explanation of Superfluid streaming on LinQup

How Real-Time Streaming Works

Deep dive into the technical architecture powering LinQup's payment streaming.

Overview

LinQup uses Superfluid Protocol to enable continuous, real-time payment streams on Polygon blockchain.

Key Concept

Instead of discrete transactions, streams are continuous agreements where funds flow every second without requiring individual transactions.

Superfluid Protocol

What is Superfluid?

Superfluid is a smart contract framework for programmable cashflows on Ethereum-compatible blockchains.

Core innovation:

  • Money streams continuously
  • No per-second transactions needed
  • Gas-efficient (one tx to start, one to stop)
  • Composable with DeFi

Constant Flow Agreements (CFA)

CFA = The streaming primitive

A CFA is a smart contract agreement between two addresses:

{
  sender: 0xABC... (supporter)
  receiver: 0xXYZ... (creator)
  flowRate: 385802469135 (wei per second)
  token: USDCx
  startTime: 1234567890
}

flowRate explained:

  • Measured in wei per second (smallest unit, 18 decimals for USDCx)
  • Example: 3,858,024,691,358,024 wei/sec ≈ $10/month
  • Immutable once created (must delete and recreate to change)

Super Tokens

What are Super Tokens?

ERC-20 tokens extended with streaming capability.

USDCx = USDC Super Token

  • Wraps regular USDC (1:1)
  • Adds streaming functions
  • Fully compatible with USDC otherwise
  • Can unwrap back anytime

Technical implementation:

// Wrapping USDC → USDCx
function upgrade(uint256 amount) external;

// Unwrapping USDCx → USDC
function downgrade(uint256 amount) external;

How Streams Work

Starting a Stream

High-level flow:

  1. User action: Click "Start Stream" in widget
  2. Approval (if first time): Approve USDCx spending
  3. Create Flow: Call Superfluid CFAv1.createFlow()
  4. Smart contract: Records agreement on-chain
  5. Streaming begins: Balance updates every second

Smart contract call:

// Simplified version
await cfaV1.createFlow({
  sender: supporterAddress,
  receiver: creatorAddress,
  superToken: usdcxAddress,
  flowRate: "3858024691358024", // wei per second for $10/month
});

On-chain state change:

mapping(address => mapping(address => int96)) flowRates;
// flowRates[supporter][creator] = 3858024691358024

Calculating Balance

Real-time balance = No database queries needed

Superfluid calculates balances mathematically:

function realtimeBalanceOf(address account) returns (uint256) {
  int256 balance = staticBalance[account];
  int96 netFlowRate = totalInflow[account] - totalOutflow[account];
  uint256 timeSinceUpdate = block.timestamp - lastUpdateTime[account];

  return balance + (netFlowRate * timeSinceUpdate);
}

Example:

  • Static balance: 100 USDCx (last recorded)
  • Net flow: -0.0000038 USDCx/sec (outflow of $10/month)
  • Time elapsed: 86400 seconds (1 day)
  • Real-time balance: 100 - (0.0000038 × 86400) = 99.67 USDCx

Updating a Stream

Cannot update flow rate directly

Must delete old flow and create new flow:

// Change from $10/month to $20/month
await cfaV1.deleteFlow({
  sender: supporterAddress,
  receiver: creatorAddress,
  superToken: usdcxAddress,
});

await cfaV1.createFlow({
  sender: supporterAddress,
  receiver: creatorAddress,
  superToken: usdcxAddress,
  flowRate: "7716049382716049", // $20/month
});

LinQup widget handles this automatically

Stopping a Stream

Simple delete operation:

await cfaV1.deleteFlow({
  sender: supporterAddress,
  receiver: creatorAddress,
  superToken: usdcxAddress,
});

What happens:

  1. Flow rate set to 0
  2. Final balances calculated
  3. Stream agreement removed
  4. Both parties can access their updated balances

Gas Efficiency

Why Streaming is Gas-Efficient

Traditional approach (sending $10/month in daily payments):

  • 30 transactions per month
  • 30 × $0.01 = $0.30 gas cost

Streaming approach:

  • 1 transaction to start
  • 1 transaction to stop
  • 2 × $0.01 = $0.02 gas cost

Savings: 93% reduction for 1-month stream

Gas Costs Breakdown

One-time setup (per wallet):

  • Wrap USDC → USDCx: ~50,000 gas
  • Approve USDCx spending: ~45,000 gas
  • Total: ~95,000 gas ≈ $0.02

Per stream:

  • Create flow: ~250,000 gas ≈ $0.01
  • Update flow: ~300,000 gas ≈ $0.015
  • Delete flow: ~100,000 gas ≈ $0.005

While streaming:

  • Gas cost per second: 0 gas
  • No transactions needed
  • Balance updates are pure calculations

Smart Contract Architecture

LinQup Contract Interactions

┌─────────────┐
│   Wallet    │
│ (MetaMask)  │
└──────┬──────┘

       ├─────────────────┐
       │                 │
       v                 v
┌──────────────┐  ┌─────────────────┐
│  USDCx Token │  │ Superfluid CFAv1│
│  (ERC-20+)   │  │  (Streaming)    │
└──────────────┘  └─────────────────┘

Key contracts on Polygon:

  • USDC: 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
  • USDCx: 0x07b24BBD834c1c546EcE89fF95f71D9F13a2eBD1
  • CFAv1: 0x6EeE6060f715257b970700bc2656De21dEdF074C

CFAv1 Interface

Core functions LinQup uses:

interface IConstantFlowAgreementV1 {
  // Create new stream
  function createFlow(
    ISuperToken token,
    address receiver,
    int96 flowRate,
    bytes calldata ctx
  ) external returns (bytes memory newCtx);

  // Update existing stream
  function updateFlow(
    ISuperToken token,
    address receiver,
    int96 flowRate,
    bytes calldata ctx
  ) external returns (bytes memory newCtx);

  // Delete stream
  function deleteFlow(
    ISuperToken token,
    address sender,
    address receiver,
    bytes calldata ctx
  ) external returns (bytes memory newCtx);

  // Query flow data
  function getFlow(
    ISuperToken token,
    address sender,
    address receiver
  ) external view returns (
    uint256 timestamp,
    int96 flowRate,
    uint256 deposit,
    uint256 owedDeposit
  );
}

Flow Rate Calculations

Converting $/month to wei/second

Formula:

function monthlyToFlowRate(dollarPerMonth: number): string {
  const SECONDS_PER_MONTH = 30 * 24 * 60 * 60; // 2,592,000
  const USDCX_DECIMALS = 18; // USDCx uses 18 decimals (standard ERC-20)

  // Convert dollars to smallest unit (wei with 18 decimals)
  const weiPerMonth = BigInt(dollarPerMonth) * (10n ** BigInt(USDCX_DECIMALS));

  // Divide by seconds in month (integer division)
  const flowRate = weiPerMonth / BigInt(SECONDS_PER_MONTH);

  return flowRate.toString();
}

// Example: $10/month
// = 10 * 10^18 wei
// = 10,000,000,000,000,000,000 / 2,592,000
// = 3,858,024,691,358,024 wei per second

Precision:

Superfluid uses int96 for flow rates:

  • Max value: 2^95 - 1 (positive flows)
  • Min value: -2^95 (negative flows)
  • Precision: 1 wei per second

Converting wei/second to $/month

Reverse formula:

function flowRateToMonthly(flowRate: string): number {
  const SECONDS_PER_MONTH = 30 * 24 * 60 * 60;
  const USDCX_DECIMALS = 18;

  const weiPerSecond = BigInt(flowRate);
  const weiPerMonth = weiPerSecond * BigInt(SECONDS_PER_MONTH);
  const dollarsPerMonth = Number(weiPerMonth) / (10 ** USDCX_DECIMALS);

  return dollarsPerMonth;
}

// Example: 3,858,024,691,358,024 wei/sec
// = 3,858,024,691,358,024 * 2,592,000
// = 9,999,999,999,999,998,208,000 wei per month
// = 9.999999999... USDCx
// ≈ $10/month

Deposits & Solvency

Security Deposit

Why deposits exist:

Superfluid requires a small deposit when creating flows to ensure solvency.

Deposit calculation:

deposit = flowRate × 3600 seconds

Example: $10/month stream

  • Flow rate: 3,858,024,691,358,024 wei/sec
  • Deposit: 3,858,024,691,358,024 × 3600 = 13,888,888,888,888,886,400 wei
  • = 0.0139 USDCx deposit (approximately 1 hour worth)

What happens to deposit:

  • Locked while stream active
  • Returned when stream stops
  • Acts as buffer for protocol

Solvency Enforcement

Critical balance = Point where account becomes insolvent

criticalBalance = -deposit

What happens at critical balance:

  1. Stream automatically stops
  2. Deposit used to pay final amounts
  3. Account marked as critical (liquidatable)
  4. Must add funds to resume

LinQup prevents this:

  • Widget shows time until funds run out
  • Warns before reaching critical balance
  • Stops stream before insolvency

Advanced: Aggregate Flows

Net Flow Rate

For accounts with multiple streams:

netFlowRate = totalInflow - totalOutflow

Example supporter streaming to 3 creators:

  • Creator A: 3,858,024,691,358,024 wei/sec ($10/month)
  • Creator B: 1,929,012,345,679,012 wei/sec ($5/month)
  • Creator C: 7,716,049,382,716,049 wei/sec ($20/month)
  • Total outflow: 13,503,086,419,753,085 wei/sec ($35/month)
  • Net flow: -13,503,086,419,753,085 wei/sec (negative = losing money)

Example creator receiving from 5 supporters:

  • Supporter 1: 3,858,024,691,358,024 wei/sec
  • Supporter 2: 7,716,049,382,716,049 wei/sec
  • Supporter 3: 1,929,012,345,679,012 wei/sec
  • Supporter 4: 3,858,024,691,358,024 wei/sec
  • Supporter 5: 11,574,074,074,074,074 wei/sec
  • Total inflow: 28,935,185,185,185,183 wei/sec ($75/month)
  • Net flow: +28,935,185,185,185,183 wei/sec (positive = earning money)

Balance Projections

Time until balance runs out:

function timeUntilEmpty(balance: bigint, netFlowRate: bigint): number {
  if (netFlowRate >= 0) return Infinity; // Earning or neutral

  const secondsUntilEmpty = balance / -netFlowRate;
  return Number(secondsUntilEmpty);
}

// Example:
// Balance: 100 USDCx = 100 * 10^18 wei = 100,000,000,000,000,000,000 wei
// Net flow: -3,858,024,691,358,024 wei/sec ($10/month outflow)
// Time: 100,000,000,000,000,000,000 / 3,858,024,691,358,024 ≈ 25,920,000 seconds
// = 432,000 minutes = 7,200 hours = 300 days (about 10 months)

Ethereum Events

Monitoring Streams

FlowUpdated event:

event FlowUpdated(
  ISuperToken indexed token,
  address indexed sender,
  address indexed receiver,
  int96 flowRate,
  int256 totalSenderFlowRate,
  int256 totalReceiverFlowRate,
  bytes userData
);

LinQup uses this to:

  • Update dashboard in real-time
  • Track stream history
  • Calculate cumulative totals
  • Trigger notifications

Querying Historical Streams

Using The Graph (Superfluid Subgraph):

query GetUserStreams($address: String!) {
  streams(where: { sender: $address }) {
    receiver {
      id
    }
    currentFlowRate
    streamedUntilUpdatedAt
    updatedAtTimestamp
  }
}

Polygon Superfluid Subgraph: https://api.thegraph.com/subgraphs/name/superfluid-finance/protocol-v1-matic

Security Considerations

Immutability

What you CAN'T do:

  • Increase someone else's outflow
  • Steal streaming funds
  • Reverse/cancel past streams
  • Deny stream deletion by sender

What you CAN do:

  • Delete your own outgoing streams anytime
  • Stop receiving (delete incoming stream)
  • Withdraw your balance anytime

Smart Contract Risks

Audited by:

  • OpenZeppelin
  • Consensys Diligence
  • Runtime Verification

Deployed contracts:

  • Immutable (cannot be upgraded maliciously)
  • Open source (verified on Polygonscan)
  • Battle-tested (millions in TVL)

Common Attacks & Mitigations

Attack: Drain wallet via stream

  • Mitigation: Only sender can create flows from their address
  • Signing: Requires wallet signature approval

Attack: Prevent stream deletion

  • Mitigation: deleteFlow always succeeds for sender
  • Protocol guarantee: Sender has absolute control

Attack: Front-run stream updates

  • Mitigation: Updates are atomic on-chain
  • MEV protection: Polygon PoS less vulnerable than mainnet

Implementation in LinQup

Widget Flow

Starting a stream from LinQup widget:

// 1. Check USDCx balance
const balance = await usdcxContract.balanceOf(userAddress);

// 2. Calculate flow rate from monthly input
const monthlyAmount = 10; // $10/month from user input
const flowRate = calculateFlowRate(monthlyAmount);

// 3. Check if flow already exists
const existingFlow = await cfaV1.getFlow({
  superToken: usdcxAddress,
  sender: userAddress,
  receiver: creatorAddress,
});

// 4. Create or update flow
if (existingFlow.flowRate === "0") {
  // Create new flow
  await cfaV1.createFlow({
    receiver: creatorAddress,
    flowRate: flowRate,
    userData: "0x", // Optional metadata
  });
} else {
  // Update existing flow (delete + create)
  await cfaV1.deleteFlow({
    sender: userAddress,
    receiver: creatorAddress,
  });

  await cfaV1.createFlow({
    receiver: creatorAddress,
    flowRate: flowRate,
  });
}

// 5. Record in LinQup database for analytics
await recordStreamStart({
  supporterId: userAddress,
  creatorId: creatorAddress,
  flowRate: flowRate,
});

Database Integration

LinQup stores:

  • Stream metadata (names, messages)
  • Historical totals
  • Supporter preferences
  • NFT associations

LinQup does NOT store:

  • Current balances (query blockchain)
  • Real-time flow rates (query Superfluid)
  • Active stream state (query CFAv1)

Why? Blockchain is source of truth for money.

Testing Streaming

Local Development

Using Hardhat + Superfluid SDK:

# Install dependencies
npm install @superfluid-finance/ethereum-contracts

# Deploy Superfluid framework locally
npx hardhat run scripts/deploy-superfluid-framework.js

# Test streaming
npx hardhat test test/streaming.test.ts

Mumbai Testnet (Polygon Testnet)

Test USDCx on Mumbai:

  • Address: 0x42bb40bF79730451B11f6De1CbA222F17b87Afd7
  • Get from faucet: https://faucet.polygon.technology/

CFAv1 on Mumbai:

  • Address: 0x49e565Ed1bdc17F3d220f72DF0857C26FA83F873

Performance Characteristics

Gas Costs (Polygon)

Typical gas prices:

  • Low: 30 gwei
  • Average: 100 gwei
  • High: 500 gwei

At 100 gwei:

  • Create flow: 250,000 gas × 100 gwei = 0.025 MATIC ≈ $0.01
  • Delete flow: 100,000 gas × 100 gwei = 0.01 MATIC ≈ $0.005

Scalability

Per-account limits:

  • Max simultaneous outflows: Unlimited (gas-limited)
  • Max simultaneous inflows: Unlimited
  • Max flow rate: 2^95 wei/sec (practically unlimited)

Network limits:

  • Polygon: ~30 TPS (conservative)
  • Superfluid operations: ~300-500 gas per second (streaming)
  • Sustainable streams per block: Thousands

Resources

Official Documentation

Developer Tools

LinQup Implementation

Next Steps