Binding Characterization: SPR and BLI

SkillDev tools

A skill for dev tools by lamm-mit.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Binding Characterization: SPR and BLI skill

What this skill tells your AI

The instructions your AI receives, as published by lamm-mit/scienceclaw in skills/binding-characterization/SKILL.md and read by ahel’s review.

Computational and experimental planning guide for Surface Plasmon Resonance (SPR) and Biolayer Interferometry (BLI) binding kinetics. Covers assay design, troubleshooting, and data interpretation for designed proteins.

SPR vs. BLI Selection

FeatureSPRBLI
ThroughputLow–mediumHigh (96/384-well)
SensitivityHigherLower
Reference subtractionFlow cellReference well
Sample consumptionMoreLess
RegenerationCriticalMore forgiving
Best forHigh-quality kineticsScreening many variants
InstrumentsBiacore (Cytiva), ProteOnOctet (Sartorius)

Assay Design Principles

Ligand vs. Analyte Choice

Ligand (immobilized on chip/tip):
  - Use the molecule you have abundant, stable supply of
  - Typically: target protein (larger, more stable)
  - Avoid: multivalent molecules on surface (avidity artifacts)

Analyte (flows in solution):
  - Typically: designed binder/antibody
  - Monovalent preferred for clean 1:1 kinetics

Immobilization Strategies (SPR)

MethodUse When
Amine coupling (NHS/EDC)Any protein, permanent
Streptavidin captureBiotinylated ligand, reversible
His-tag captureHis-tagged ligand, regenerable
Protein A/GAntibody Fc capture
# Target immobilization levels (Biacore)
# RMax = Rligand × (MW_analyte / MW_ligand) × stoichiometry
# Target: RMax = 50–200 RU for kinetics (avoid mass transport)

def calc_immobilization_target(
    mw_ligand: float,    # kDa
    mw_analyte: float,   # kDa
    target_rmax: float = 100,   # RU
    stoichiometry: float = 1.0
) -> float:
    """Calculate required ligand immobilization level (RU)."""
    return target_rmax * (mw_ligand / mw_analyte) / stoichiometry

Kinetic Experiment Design

# Analyte concentration series for kinetics
# Span at least 10x above and below Kd
# Use 6–8 concentrations in 2–3x dilution series

def design_concentration_series(
    estimated_kd: float,   # M, rough estimate
    n_points: int = 7,
    dilution_factor: float = 3.0
) -> list:
    """Design analyte concentration series spanning Kd."""
    import numpy as np
    # Center series around Kd
    top = estimated_kd * 30
    bottom = estimated_kd / 30
    concs = [top / (dilution_factor**i) for i in range(n_points)]
    return concs

# Example: estimated Kd = 10 nM
concs = design_concentration_series(10e-9)
# → [300nM, 100nM, 33nM, 11nM, 3.7nM, 1.2nM, 0.4nM]

Data Analysis

import numpy as np
from scipy.optimize import curve_fit

def fit_1to1_kinetics(time: np.ndarray, response: np.ndarray,
                      conc: float) -> dict:
    """
    Fit simple 1:1 Langmuir binding model to SPR/BLI data.
    Returns ka, kd, Rmax.
    """
    def binding_model(t, ka, kd, rmax):
        kobs = ka * conc + kd
        return rmax * (ka * conc / kobs) * (1 - np.exp(-kobs * t))

    popt, pcov = curve_fit(
        binding_model, time, response,
        p0=[1e5, 1e-3, 100],
        bounds=([0, 0, 0], [1e8, 1, 1000])
    )
    ka, kd, rmax = popt
    kd_eq = kd / ka  # Equilibrium Kd = kd/ka

    return {
        "ka": ka,          # M⁻¹s⁻¹ (association rate)
        "kd": kd,          # s⁻¹ (dissociation rate)
        "Kd": kd_eq,       # M (equilibrium dissociation constant)
        "Rmax": rmax,      # RU
        "t_half_dissoc": np.log(2) / kd  # seconds
    }

Troubleshooting

ProblemCauseFix
No binding signalWrong orientation/inactivationSwitch ligand/analyte; check activity
Biphasic associationHeterogeneous ligand or two-stateUse fresh surface; check protein homogeneity
Incomplete dissociationVery slow kd or non-specificExtend dissociation time; add 0.05% Tween-20
Bulk refractive index shiftBuffer mismatchMatch buffer exactly; increase reference subtraction
High non-specific bindingCharge interactionsAdd 0.5 M NaCl; block with BSA/ethanolamine
Hook effectToo high analyte concReduce top concentration 10x
Mass transport limitationToo much ligandReduce immobilization level; increase flow rate
Poor regenerationHarsh conditionsTest HCl 10mM; glycine pH 2.0; NaOH 10mM

Interpreting Results

def interpret_binding(ka: float, kd: float, kd_eq: float) -> dict:
    """Classify binding kinetics."""
    return {
        "Kd_nM": kd_eq * 1e9,
        "affinity_class": (
            "picomolar" if kd_eq < 1e-10 else
            "nanomolar" if kd_eq < 1e-7 else
            "micromolar" if kd_eq < 1e-4 else
            "weak/non-specific"
        ),
        "kinetic_class": (
            "fast_on_fast_off" if ka > 1e6 and kd > 1e-2 else
            "fast_on_slow_off" if ka > 1e6 and kd < 1e-3 else
            "slow_on_slow_off" if ka < 1e5 else "moderate"
        ),
        "t_half_hours": (np.log(2) / kd) / 3600,
        "diffusion_limited": ka > 1e7,  # Possible mass transport if True
    }

Typical Results for Designed Proteins

Design methodTypical Kd rangeNotes
RFdiffusion + MPNN (first round)10–100 µMNeeds optimization
BoltzGen (first round)1–10 µMBetter starting point
After affinity maturation1–100 nMTarget for therapeutic proteins
Antibodies (CDR design)0.1–10 nMMature

Signals

GitHub stars
242
Forks
42
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
binding-characterization-lamm-mit
Source
github.com/lamm-mit/scienceclaw