diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/README b/examples/ChemistryTests/MetalAdvectionTestGEAR/README
new file mode 100644
index 0000000000000000000000000000000000000000..5f16d8ee8e7899fb8ac6db1bfbb504e1821dae89
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/README
@@ -0,0 +1,23 @@
+Metal advection test for the GEAR/AGORA chemistry schemes
+
+In some schemes, like the meshless (gizmo) scheme, particles exchange 
+masses via fluxes. This example tests whether the metals are correctly
+advected with those mass fluxes.
+
+To run this test with GEAR, compile with:
+    --with-hydro-dimension=2 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-chemistry=GEAR_X
+with X the desired number of elements. The number of elements must be also be set in the makeIC.py script
+and must be the equal to one used in the compilation flag. The default value is 4.
+
+To run this test with AGORA, compile with:
+    --with-hydro-dimension=2 --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-chemistry=AGORA
+NOTE: The AGORA scheme only traces Fe mass and total metal mass, so the number of elements in makeIC.py must be set
+to the value of 2.
+
+Due to the nature of this test, not much mass will be exchanged when running in Lagrangian mode,
+and hence not much advection will happen.
+To be able to see the effect of the advection, the hydrodynamics must be run in Eulerian mode.
+E.g. for gizmo-mvf: uncomment `#define GIZMO_FIX_PARTICLES` in src/const.h.
+
+Expected output when running in Eulerian mode is that the profiles should maintain their shape,
+but edges will be blurred due to numerical diffusion.
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/advect_metals.yml b/examples/ChemistryTests/MetalAdvectionTestGEAR/advect_metals.yml
new file mode 100644
index 0000000000000000000000000000000000000000..de31d05b432d578bfa5bd8cc7c3ba5eca828a9e8
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/advect_metals.yml
@@ -0,0 +1,46 @@
+MetaData:
+  run_name: advect_metals_GEAR
+
+# Define the system of units to use internally. 
+InternalUnitSystem:
+  UnitMass_in_cgs:     1   # Grams
+  UnitLength_in_cgs:   1   # Centimeters
+  UnitVelocity_in_cgs: 1   # Centimeters per second
+  UnitCurrent_in_cgs:  1   # Amperes
+  UnitTemp_in_cgs:     1   # Kelvin
+
+# Parameters governing the time integration
+TimeIntegration:
+  time_begin: 0.    # The starting time of the simulation (in internal units).
+  time_end:   0.1   # The end time of the simulation (in internal units).
+  dt_min:     1e-9  # The minimal time-step size of the simulation (in internal units).
+  dt_max:     1e-1  # The maximal time-step size of the simulation (in internal units).
+
+
+# Parameters governing the snapshots
+Snapshots:
+  basename:            output # Common part of the name of output files
+  time_first:          0.     # Time of the first output (in internal units)
+  delta_time:          0.1    # Time between snapshots
+
+# Parameters governing the conserved quantities statistics
+Statistics:
+  time_first:          0.
+  delta_time:          1e-1   # Time between statistics output
+
+# Parameters for the hydrodynamics scheme
+SPH:
+  resolution_eta:        1.2348   # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel).
+  CFL_condition:         0.1      # Courant-Friedrich-Levy condition for time integration.
+
+# Parameters related to the initial conditions
+InitialConditions:
+  file_name:  ./advect_metals.hdf5     # The file to read
+  periodic:   1                        # periodic ICs.
+
+GEARChemistry:
+  initial_metallicity: -1         # Negative values mean that the metallicity will be read from the ICs
+
+AGORAChemistry:
+  initial_metallicity: -1         # Negative values mean that the metallicity will be read from the ICs
+  scale_initial_metallicity: 1    # scale the initial metallicity using solar_abundance_Metals? (needed to prevent crash in writing of metadata)
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/getGlass.sh b/examples/ChemistryTests/MetalAdvectionTestGEAR/getGlass.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a958311b6de07663c7f7c70cff9e95d86ce3efb2
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/getGlass.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ICs/glassPlane_64.hdf5
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/makeIC.py b/examples/ChemistryTests/MetalAdvectionTestGEAR/makeIC.py
new file mode 100755
index 0000000000000000000000000000000000000000..12810acc5599e2d8d0a51505533b6fabb69ee7a2
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/makeIC.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+import math
+
+# ---------------------------------------------------------------------
+# Test the diffusion/advection of metals by creating regions with/without
+# initial metallicities. Run with EAGLE chemistry.
+# ---------------------------------------------------------------------
+
+import numpy as np
+import h5py
+
+GAMMA = 5 / 3
+RHO = 1
+P = 1
+VELOCITY = 2.5
+# Set ELEMENT_COUNT to 2 for AGORA runs, or for GEAR compile swift with `--with-chemistry=GEAR_X` with X equal to
+# ELEMENT_COUNT.
+ELEMENT_COUNT = 4
+
+outputfilename = "advect_metals.hdf5"
+
+
+def get_mask(boxsize, pos, element_idx):
+    x = boxsize[0]
+    right_half_mask = pos[:, 0] > x / 3
+
+    if element_idx == ELEMENT_COUNT - 1:
+        return right_half_mask
+    else:
+        periods = 2 * (element_idx + 2)
+        periods_mask = (pos[:, 0] * periods // x) % 2 == 0
+        return right_half_mask & periods_mask
+
+
+def get_abundance(element_idx):
+    if element_idx == ELEMENT_COUNT - 1:
+        return 0.2
+    else:
+        return 0.1 * 0.5 ** element_idx
+
+
+def get_element_abundances_metallicity(pos, boxsize):
+    element_abundances = []
+    for i in range(ELEMENT_COUNT):
+        element_abundances.append(np.where(get_mask(boxsize, pos, i), get_abundance(i), 0))
+
+    return np.stack(element_abundances, axis=1)
+
+
+if __name__ == "__main__":
+    glass = h5py.File("glassPlane_64.hdf5", "r")
+    parts = glass["PartType0"]
+    pos = parts["Coordinates"][:]
+    pos = np.concatenate([pos, pos + np.array([1, 0, 0])])
+    h = parts["SmoothingLength"][:]
+    h = np.concatenate([h, h])
+    glass.close()
+
+    # Set up metadata
+    boxsize = np.array([2.0, 1.0, 1.0])
+    n_part = len(h)
+    ids = np.arange(n_part) + 1
+
+    # Setup other particle quantities
+    rho = RHO * np.ones_like(h)
+    rho[pos[:, 1] < 0.5 * boxsize[1]] *= 0.5
+    masses = rho * np.prod(boxsize) / n_part
+    velocities = np.zeros((n_part, 3))
+    velocities[:, :] = 0.5 * math.sqrt(2) * VELOCITY * np.array([1., 1., 0.])
+    internal_energy = P / (rho * (GAMMA - 1))
+
+    # Setup metallicities
+    metallicities = get_element_abundances_metallicity(pos, boxsize)
+
+    # Now open the file and write the data.
+    file = h5py.File(outputfilename, "w")
+
+    # Header
+    grp = file.create_group("/Header")
+    grp.attrs["BoxSize"] = boxsize
+    grp.attrs["NumPart_Total"] = [n_part, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_Total_HighWord"] = [0, 0, 0, 0, 0, 0]
+    grp.attrs["NumPart_ThisFile"] = [n_part, 0, 0, 0, 0, 0]
+    grp.attrs["Time"] = 0.0
+    grp.attrs["NumFilesPerSnapshot"] = 1
+    grp.attrs["MassTable"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+    grp.attrs["Flag_Entropy_ICs"] = 0
+    grp.attrs["Dimension"] = 2
+
+    # Units
+    grp = file.create_group("/Units")
+    grp.attrs["Unit length in cgs (U_L)"] = 1.0
+    grp.attrs["Unit mass in cgs (U_M)"] = 1.0
+    grp.attrs["Unit time in cgs (U_t)"] = 1.0
+    grp.attrs["Unit current in cgs (U_I)"] = 1.0
+    grp.attrs["Unit temperature in cgs (U_T)"] = 1.0
+
+    # Particle group
+    grp = file.create_group("/PartType0")
+    grp.create_dataset("Coordinates", data=pos, dtype="d")
+    grp.create_dataset("Velocities", data=velocities, dtype="f")
+    grp.create_dataset("Masses", data=masses, dtype="f")
+    grp.create_dataset("SmoothingLength", data=h, dtype="f")
+    grp.create_dataset("InternalEnergy", data=internal_energy, dtype="f")
+    grp.create_dataset("ParticleIDs", data=ids, dtype="L")
+    grp.create_dataset("MetalMassFraction", data=metallicities, dtype="f")
+
+    file.close()
+
+    # close up, and we're done!
+    file.close()
+
+    if ELEMENT_COUNT == 2:
+        print(f"Make sure swift was compiled with `--with-chemistry=GEAR_2` or `--with-chemistry=AGORA`")
+    else:
+        print(f"Make sure swift was compiled with `--with-chemistry=GEAR_{ELEMENT_COUNT}`")
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/plotAdvectedMetals.py b/examples/ChemistryTests/MetalAdvectionTestGEAR/plotAdvectedMetals.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4415303150d8af155f7b54bd93d965ff3fece86
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/plotAdvectedMetals.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import math
+
+import matplotlib.pyplot as plt
+from matplotlib.cm import ScalarMappable
+from matplotlib.colors import SymLogNorm, Normalize
+import numpy as np
+import unyt
+
+import swiftsimio
+from swiftsimio.visualisation import project_gas
+from pathlib import Path
+
+from makeIC import VELOCITY, ELEMENT_COUNT
+
+
+def plot_single(ax_mass, ax_fraction, name, title, data, mass_map, kwargs_inner):
+    mass_weighted_map = project_gas(data, project=name, **kwargs_inner["projection"])
+    ax_mass.imshow(mass_weighted_map.in_cgs().value.T, **kwargs_inner["imshow_mass"])
+    ax_mass.set_title(title)
+    ax_fraction.imshow((mass_weighted_map / mass_map).value.T, **kwargs_inner["imshow_fraction"])
+    ax_fraction.set_title(title)
+    ax_mass.axis("off")
+    ax_fraction.axis("off")
+
+
+def plot_all(fname, savename):
+    data = swiftsimio.load(fname)
+
+    # Shift coordinates to starting position
+    velocity = 0.5 * math.sqrt(2) * VELOCITY * np.array([1, 1, 0]) * unyt.cm / unyt.s
+    data.gas.coordinates -= data.metadata.time * velocity
+
+    # Add mass weighted element mass fractions to gas dataset
+    masses = data.gas.masses
+    element_mass_fractions = data.gas.metal_mass_fractions
+    columns = getattr(element_mass_fractions, "named_columns", None)
+    for i in range(ELEMENT_COUNT):
+        if columns is None:
+            data.gas.__setattr__(f"element_mass_sp{i}", element_mass_fractions[:, i] * masses)
+        else:
+            data.gas.__setattr__(f"element_mass_sp{i}", getattr(element_mass_fractions, columns[i]) * masses)
+
+    # Create necessary figures and axes
+    fig = plt.figure(layout="constrained", figsize=(8, 2 * ELEMENT_COUNT + 1))
+    fig.suptitle(f"Profiles shifted to starting position after t={data.metadata.time:.2f}", fontsize=14)
+    fig_ratios, fig_masses = fig.subfigures(1, 2)
+    fig_ratios.suptitle("Mass ratio of elements")
+    fig_masses.suptitle("Surface density in elements")
+    axes_ratios = fig_ratios.subplots(ELEMENT_COUNT, 1, sharex=True, sharey=True)
+    axes_masses = fig_masses.subplots(ELEMENT_COUNT, 1, sharex=True, sharey=True)
+
+    # parameters for swiftsimio projections
+    projection_kwargs = {
+        "region": np.array([0, 2, 0, 1, 0, 1]) * unyt.cm,
+        "resolution": 500,
+        "parallel": True
+    }
+    # Parameters for imshow
+    if ELEMENT_COUNT > 5:
+        thresh = 10 ** math.floor(math.log10(0.5 ** (ELEMENT_COUNT - 2)))
+        norm_ratios = SymLogNorm(vmin=0, vmax=.21, linthresh=thresh, base=10)
+        norm_masses = SymLogNorm(vmin=0, vmax=.25, linthresh=thresh, base=10)
+    else:
+        norm_ratios = Normalize(vmin=0, vmax=.21)
+        norm_masses = Normalize(vmin=0, vmax=.25)
+    imshow_fraction_kwargs = dict(norm=norm_ratios, cmap="rainbow")
+    imshow_mass_kwargs = dict(norm=norm_masses, cmap="turbo")
+
+    # Plot the quantities
+    mass_map = project_gas(data, project="masses", **projection_kwargs)
+
+    # Parameters for plotting:
+    plotting_kwargs = dict(
+        data=data,
+        mass_map=mass_map,
+        kwargs_inner=dict(
+            projection=projection_kwargs,
+            imshow_mass=imshow_mass_kwargs,
+            imshow_fraction=imshow_fraction_kwargs,
+        )
+    )
+
+    if columns is None:
+        columns = [f"Species {i + 1}" for i in range(ELEMENT_COUNT)]
+    for i in range(ELEMENT_COUNT):
+        plot_single(axes_masses[i], axes_ratios[i], f"element_mass_sp{i}", columns[i], **plotting_kwargs)
+
+    # Add Colorbars
+    cb_masses = fig_masses.colorbar(ScalarMappable(**imshow_mass_kwargs), orientation="horizontal", shrink=0.75,
+                                    pad=0.01, ax=axes_masses)
+    cb_ratios = fig_ratios.colorbar(ScalarMappable(**imshow_fraction_kwargs), orientation="horizontal", shrink=0.75,
+                                    pad=0.01, ax=axes_ratios)
+    cb_masses.ax.set_xlabel("Surface density (g/cm^2)")
+    cb_ratios.ax.set_xlabel("Mass ratio")
+
+    # Save output
+    fig.savefig(savename, dpi=300)
+
+
+if __name__ == "__main__":
+    cwd = Path(__file__).parent
+    print("Plotting metals for output_0000.hdf5...")
+    plot_all(cwd / "output_0000.hdf5", savename=cwd / "metal_advection_0000.png")
+    print("Plotting metals for output_0001.hdf5...")
+    plot_all(cwd / "output_0001.hdf5", savename=cwd / "metal_advection_0001.png")
+    print("Done!")
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/run.sh b/examples/ChemistryTests/MetalAdvectionTestGEAR/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..27a008f3d7bac0d0931372aeec34512af257616e
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/run.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# make run.sh fail if a subcommand fails
+set -e
+set -o pipefail
+
+if [ ! -e glassPlane_64.hdf5 ]
+then
+    echo "Fetching initial glass file ..."
+    ./getGlass.sh
+fi
+
+# Always generate ICs in case ELEMENT_COUNT has been changed
+echo "Generating ICs"
+python3 makeIC.py
+
+# Run SWIFT (must be compiled with --with-chemistry=GEAR_X or --with-chemistry=AGORA)
+../../../swift \
+    --hydro --threads=4 advect_metals.yml 2>&1 | tee output.log
+
+python3 runSanityChecksAdvectedMetals.py
+python3 plotAdvectedMetals.py
diff --git a/examples/ChemistryTests/MetalAdvectionTestGEAR/runSanityChecksAdvectedMetals.py b/examples/ChemistryTests/MetalAdvectionTestGEAR/runSanityChecksAdvectedMetals.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e684a9bb0dc592e3f99c3354c7dfe5310dd71a1
--- /dev/null
+++ b/examples/ChemistryTests/MetalAdvectionTestGEAR/runSanityChecksAdvectedMetals.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+###############################################################################
+# This file is part of SWIFT.
+# Copyright (c) 2024 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from pathlib import Path
+
+import numpy as np
+import swiftsimio
+
+from makeIC import ELEMENT_COUNT
+
+def test(name):
+    def test_decorator(test_func):
+        def test_runner(*args, **kwargs):
+            try:
+                test_func(*args, **kwargs)
+            except Exception as e:
+                print(f"\tTest {name} failed!")
+                raise e
+            else:
+                print(f"\tTest {name} \u2705")
+
+        return test_runner
+
+    return test_decorator
+
+
+def approx_equal(a, b, threshold=0.001):
+    return 2 * abs(a - b) / (abs(a) + abs(b)) < threshold
+
+
+@test("total metal mass conservation")
+def test_total_metal_mass_conservation(data_start, data_end):
+    def metal_mass(data):
+        columns = getattr(data.gas.metal_mass_fractions, "named_columns", None)
+        if columns is not None:
+            return sum(data.gas.masses * getattr(data.gas.metal_mass_fractions, c) for c in columns)
+        else:
+            return data.gas.masses.reshape(-1, 1) * data.gas.metal_mass_fractions
+
+    assert approx_equal(np.sum(metal_mass(data_start)), np.sum(metal_mass(data_end)))
+
+
+def element_mass(data, element_idx):
+    columns = getattr(data.gas.metal_mass_fractions, "named_columns", None)
+    if columns is not None:
+        return data.gas.masses * getattr(data.gas.metal_mass_fractions, columns[element_idx])
+    else:
+        return data.gas.masses * data.gas.metal_mass_fractions[:, element_idx]
+
+
+@test("element-wise mass conservation")
+def test_element_wise_mass_conservation(data_start, data_end):
+    for i in range(ELEMENT_COUNT):
+        assert approx_equal(
+            np.sum(element_mass(data_start, i)),
+            np.sum(element_mass(data_end, i))
+        )
+
+
+if __name__ == "__main__":
+    print("Running sanity checks...")
+
+    cwd = Path(__file__).parent
+    start = swiftsimio.load(cwd / "output_0000.hdf5")
+    end = swiftsimio.load(cwd / "output_0001.hdf5")
+
+    test_total_metal_mass_conservation(start, end)
+    test_element_wise_mass_conservation(start, end)
+
+    print("Done!")
diff --git a/src/chemistry/AGORA/chemistry_additions.h b/src/chemistry/AGORA/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..d166dc0debef6073f1913c6071a7945854980075
--- /dev/null
+++ b/src/chemistry/AGORA/chemistry_additions.h
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_AGORA_ADDITIONS_H
+#define SWIFT_CHEMISTRY_AGORA_ADDITIONS_H
+
+/**
+ * @brief Resets the metal mass fluxes for schemes that use them.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_reset_mass_fluxes(
+    struct part* restrict p) {
+  for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+    p->chemistry_data.metal_mass_fluxes[i] = 0.f;
+  }
+}
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {
+  /* For hydro schemes that exchange mass fluxes between the particles,
+   * we want to advect the metals. */
+  if (p->flux.dt > 0.) {
+
+    /* Check for vacuum? */
+    if (p->conserved.mass + p->flux.mass <= 0.) {
+      for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+        p->chemistry_data.metal_mass[i] = 0.;
+        p->chemistry_data.smoothed_metal_mass_fraction[i] = 0.;
+      }
+      chemistry_reset_mass_fluxes(p);
+      /* Nothing left to do */
+      return;
+    }
+
+    /* apply the metal mass fluxes and reset them */
+    const double* metal_fluxes = p->chemistry_data.metal_mass_fluxes;
+    for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+      p->chemistry_data.metal_mass[i] =
+          fmax(p->chemistry_data.metal_mass[i] + metal_fluxes[i], 0.);
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+    const double iron_mass = p->chemistry_data.metal_mass[0];
+    const double total_metal_mass =
+        p->chemistry_data.metal_mass[AGORA_CHEMISTRY_ELEMENT_COUNT - 1];
+    if (iron_mass > total_metal_mass)
+      error("Iron mass grew larger than total metal mass!");
+    if (total_metal_mass > (p->conserved.mass + p->flux.mass) * (1. + 1.e-4))
+      error("Total metal mass grew larger than total particle mass!");
+#endif
+    chemistry_reset_mass_fluxes(p);
+  }
+}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * Metals are advected. I.e. a particle loses metals according to its own
+ * metal mass fractions and gains mass according to the neighboring particle's
+ * mass fractions.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {
+
+  const double mass_flux_integrated = mass_flux * flux_dt;
+  const float mi = pi->conserved.mass;
+  const float mi_inv = mi > 0 ? 1.f / mi : 0.f;
+  const float mj = pj->conserved.mass;
+  const float mj_inv = mj > 0 ? 1.f / mj : 0.f;
+
+  /* Convention: a positive mass flux means that pi is losing said mass and pj
+   * is gaining it. */
+  if (mass_flux > 0.f) {
+    /* pi is losing mass */
+    for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+    }
+  } else {
+    /* pi is gaining mass: */
+    for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+    }
+  }
+
+  /* update pj as well, even if it is inactive (flux.dt < 0.) */
+  if (mode == 1 || pj->flux.dt < 0.f) {
+    if (mass_flux > 0.f) {
+      /* pj is gaining mass */
+      for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+      }
+    } else {
+      /* pj is losing mass */
+      for (int i = 0; i < AGORA_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+      }
+    }
+  }
+}
+
+#endif  // SWIFT_CHEMISTRY_AGORA_ADDITIONS_H
diff --git a/src/chemistry/AGORA/chemistry_struct.h b/src/chemistry/AGORA/chemistry_struct.h
index 5bd442d6b91858bd529a482f8bf9ac066845cd42..4c49179ff0ceef279b4354b2dd8bde2fe6d8e638 100644
--- a/src/chemistry/AGORA/chemistry_struct.h
+++ b/src/chemistry/AGORA/chemistry_struct.h
@@ -46,6 +46,11 @@ struct chemistry_part_data {
   /*! Total mass of element in a particle. */
   double metal_mass[AGORA_CHEMISTRY_ELEMENT_COUNT];
 
+#ifdef HYDRO_DOES_MASS_FLUX
+  /*! Mass fluxes of the metals in a given element */
+  double metal_mass_fluxes[AGORA_CHEMISTRY_ELEMENT_COUNT];
+#endif
+
   /*! Smoothed fraction of the particle mass in a given element */
   double smoothed_metal_mass_fraction[AGORA_CHEMISTRY_ELEMENT_COUNT];
 };
diff --git a/src/chemistry/EAGLE/chemistry_additions.h b/src/chemistry/EAGLE/chemistry_additions.h
index 35f660e5a17321183c8f3e4296976a7dfc8289aa..21fbc402fc2715b11b1dcc861e6cf6d22ec14cc9 100644
--- a/src/chemistry/EAGLE/chemistry_additions.h
+++ b/src/chemistry/EAGLE/chemistry_additions.h
@@ -17,8 +17,8 @@
  *
  ******************************************************************************/
 
-#ifndef SWIFT_EAGLE_CHEMISTRY_ADDITIONS_H
-#define SWIFT_EAGLE_CHEMISTRY_ADDITIONS_H
+#ifndef SWIFT_CHEMISTRY_EAGLE_ADDITIONS_H
+#define SWIFT_CHEMISTRY_EAGLE_ADDITIONS_H
 
 /**
  * @brief Resets the metal mass fluxes for schemes that use them.
@@ -180,4 +180,4 @@ __attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
   }
 }
 
-#endif  // SWIFT_EAGLE_CHEMISTRY_ADDITIONS_H
+#endif  // SWIFT_CHEMISTRY_EAGLE_ADDITIONS_H
diff --git a/src/chemistry/GEAR/chemistry_additions.h b/src/chemistry/GEAR/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..a872e636a57e37a80c02411dd0a3b208ef747a31
--- /dev/null
+++ b/src/chemistry/GEAR/chemistry_additions.h
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_GEAR_ADDITIONS_H
+#define SWIFT_CHEMISTRY_GEAR_ADDITIONS_H
+
+/**
+ * @brief Resets the metal mass fluxes for schemes that use them.
+ *
+ * @param p The particle to act upon.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_reset_mass_fluxes(
+    struct part* restrict p) {
+  for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+    p->chemistry_data.metal_mass_fluxes[i] = 0.f;
+  }
+}
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {
+  /* For hydro schemes that exchange mass fluxes between the particles,
+   * we want to advect the metals. */
+  if (p->flux.dt > 0.) {
+
+    /* Check for vacuum? */
+    if (p->conserved.mass + p->flux.mass <= 0.) {
+      for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+        p->chemistry_data.metal_mass[i] = 0.;
+        p->chemistry_data.smoothed_metal_mass_fraction[i] = 0.;
+      }
+      chemistry_reset_mass_fluxes(p);
+      /* Nothing left to do */
+      return;
+    }
+
+    /* apply the metal mass fluxes and reset them */
+    const double* metal_fluxes = p->chemistry_data.metal_mass_fluxes;
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+      p->chemistry_data.metal_mass[i] =
+          fmax(p->chemistry_data.metal_mass[i] + metal_fluxes[i], 0.);
+    }
+
+#ifdef SWIFT_DEBUG_CHECKS
+    double sum = 0.;
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT - 1; i++) {
+      sum += p->chemistry_data.metal_mass[i];
+    }
+    const double total_metal_mass =
+        p->chemistry_data.metal_mass[GEAR_CHEMISTRY_ELEMENT_COUNT - 1];
+    if (sum > total_metal_mass)
+      error(
+          "Sum of element-wise metal masses grew larger than total metal "
+          "mass!");
+    if (total_metal_mass > p->conserved.mass + p->flux.mass)
+      error("Total metal mass grew larger than the particle mass!");
+#endif
+    chemistry_reset_mass_fluxes(p);
+  }
+}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * Metals are advected. I.e. a particle loses metals according to its own
+ * metal mass fractions and gains mass according to the neighboring particle's
+ * mass fractions.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {
+
+  const double mass_flux_integrated = mass_flux * flux_dt;
+  const float mi = pi->conserved.mass;
+  const float mi_inv = mi > 0 ? 1.f / mi : 0.f;
+  const float mj = pj->conserved.mass;
+  const float mj_inv = mj > 0 ? 1.f / mj : 0.f;
+
+  /* Convention: a positive mass flux means that pi is losing said mass and pj
+   * is gaining it. */
+  if (mass_flux > 0.f) {
+    /* pi is losing mass */
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+    }
+  } else {
+    /* pi is gaining mass: */
+    for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+      pi->chemistry_data.metal_mass_fluxes[i] -=
+          mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+    }
+  }
+
+  /* update pj as well, even if it is inactive (flux.dt < 0.) */
+  if (mode == 1 || pj->flux.dt < 0.f) {
+    if (mass_flux > 0.f) {
+      /* pj is gaining mass */
+      for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pi->chemistry_data.metal_mass[i] * mi_inv;
+      }
+    } else {
+      /* pj is losing mass */
+      for (int i = 0; i < GEAR_CHEMISTRY_ELEMENT_COUNT; i++) {
+        pj->chemistry_data.metal_mass_fluxes[i] +=
+            mass_flux_integrated * pj->chemistry_data.metal_mass[i] * mj_inv;
+      }
+    }
+  }
+}
+
+#endif  // SWIFT_CHEMISTRY_GEAR_ADDITIONS_H
diff --git a/src/chemistry/GEAR/chemistry_struct.h b/src/chemistry/GEAR/chemistry_struct.h
index 3b26c112192b29100760aeec5062359bf3dba081..df03ebd2de53a539f5b7516d581c63a8f31a0520 100644
--- a/src/chemistry/GEAR/chemistry_struct.h
+++ b/src/chemistry/GEAR/chemistry_struct.h
@@ -44,6 +44,11 @@ struct chemistry_part_data {
   /*! Total mass of element in a particle. */
   double metal_mass[GEAR_CHEMISTRY_ELEMENT_COUNT];
 
+#ifdef HYDRO_DOES_MASS_FLUX
+  /*! Mass fluxes of the metals in a given element */
+  double metal_mass_fluxes[GEAR_CHEMISTRY_ELEMENT_COUNT];
+#endif
+
   /*! Smoothed fraction of the particle mass in a given element */
   double smoothed_metal_mass_fraction[GEAR_CHEMISTRY_ELEMENT_COUNT];
 };
diff --git a/src/chemistry/QLA/chemistry_additions.h b/src/chemistry/QLA/chemistry_additions.h
new file mode 100644
index 0000000000000000000000000000000000000000..2cfcddeee2d9d57fd62203bddf8f17c4a7f6ebc0
--- /dev/null
+++ b/src/chemistry/QLA/chemistry_additions.h
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * This file is part of SWIFT.
+ * Copyright (c) 2023 Yolan Uyttenhove (yolan.uyttenhove@ugent.be)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ ******************************************************************************/
+
+#ifndef SWIFT_CHEMISTRY_QLA_ADDITIONS_H
+#define SWIFT_CHEMISTRY_QLA_ADDITIONS_H
+
+/**
+ * @brief Extra operations done during the kick. This needs to be
+ * done before the particle mass is updated in the hydro_kick_extra.
+ *
+ * @param p Particle to act upon.
+ * @param dt_therm Thermal energy time-step @f$\frac{dt}{a^2}@f$.
+ * @param dt_grav Gravity time-step @f$\frac{dt}{a}@f$.
+ * @param dt_hydro Hydro acceleration time-step
+ * @f$\frac{dt}{a^{3(\gamma{}-1)}}@f$.
+ * @param dt_kick_corr Gravity correction time-step @f$adt@f$.
+ * @param cosmo Cosmology.
+ * @param hydro_props Additional hydro properties.
+ */
+__attribute__((always_inline)) INLINE static void chemistry_kick_extra(
+    struct part* p, float dt_therm, float dt_grav, float dt_hydro,
+    float dt_kick_corr, const struct cosmology* cosmo,
+    const struct hydro_props* hydro_props) {}
+
+/**
+ * @brief update metal mass fluxes between two interacting particles during
+ * hydro_iact_(non)sym(...) calls.
+ *
+ * @param pi first interacting particle
+ * @param pj second interacting particle
+ * @param mass_flux the mass flux between these two particles.
+ * @param flux_dt the time-step over which the fluxes are exchanged
+ * @param mode 0: non-symmetric interaction, update i only. 1: symmetric
+ * interaction.
+ **/
+__attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
+    struct part* restrict pi, struct part* restrict pj, float mass_flux,
+    float flux_dt, int mode) {}
+
+#endif  // SWIFT_CHEMISTRY_QLA_ADDITIONS_H
diff --git a/src/chemistry/none/chemistry_additions.h b/src/chemistry/none/chemistry_additions.h
index 7c1d1536512e23bf939dc5bad3aded6f0ce46b73..3401dd245ff02b9f39d414317a553d260c05f470 100644
--- a/src/chemistry/none/chemistry_additions.h
+++ b/src/chemistry/none/chemistry_additions.h
@@ -17,8 +17,8 @@
  *
  ******************************************************************************/
 
-#ifndef SWIFT_NONE_CHEMISTRY_ADDITIONS_H
-#define SWIFT_NONE_CHEMISTRY_ADDITIONS_H
+#ifndef SWIFT_CHEMISTRY_NONE_ADDITIONS_H
+#define SWIFT_CHEMISTRY_NONE_ADDITIONS_H
 
 /**
  * @brief Extra operations done during the kick. This needs to be
@@ -53,4 +53,4 @@ __attribute__((always_inline)) INLINE static void runner_iact_chemistry_fluxes(
     struct part* restrict pi, struct part* restrict pj, float mass_flux,
     float flux_dt, int mode) {}
 
-#endif  // SWIFT_NONE_CHEMISTRY_ADDITIONS_H
+#endif  // SWIFT_CHEMISTRY_NONE_ADDITIONS_H
diff --git a/src/chemistry_additions.h b/src/chemistry_additions.h
index 514931def45d7a86a5c255fbb7dddf13b34406fe..ae8f3bfa3658898286cd6e88eb87cf6d5eac39f2 100644
--- a/src/chemistry_additions.h
+++ b/src/chemistry_additions.h
@@ -32,10 +32,16 @@
 
 #ifdef HYDRO_DOES_MASS_FLUX
 /* Import the right chemistry definition */
-#if defined(CHEMISTRY_NONE)
-#include "./chemistry/none/chemistry_additions.h"
+#if defined(CHEMISTRY_AGORA)
+#include "./chemistry/AGORA/chemistry_additions.h"
 #elif defined(CHEMISTRY_EAGLE)
 #include "./chemistry/EAGLE/chemistry_additions.h"
+#elif defined(CHEMISTRY_GEAR)
+#include "./chemistry/GEAR/chemistry_additions.h"
+#elif defined(CHEMISTRY_NONE)
+#include "./chemistry/none/chemistry_additions.h"
+#elif defined(CHEMISTRY_NONE)
+#include "./chemistry/QLA/chemistry_additions.h"
 #else
 #error "Metal advection unimpmlemented for selected chemistry scheme!"
 #endif