diff --git a/SVSim.Database/Models/Config/MatchingConfig.cs b/SVSim.Database/Models/Config/MatchingConfig.cs
new file mode 100644
index 0000000..11229ad
--- /dev/null
+++ b/SVSim.Database/Models/Config/MatchingConfig.cs
@@ -0,0 +1,20 @@
+namespace SVSim.Database.Models.Config;
+
+///
+/// Tunables for the in-process pair-up matching service. Today: just the AI-fallback
+/// threshold for rank-battle modes. The full matching-queue API is a separate spec;
+/// this config section lives alongside the placeholder.
+///
+[ConfigSection("Matching")]
+public class MatchingConfig
+{
+ ///
+ /// How long (seconds) a viewer must have been parked in a PvpFirstThenAiFallback
+ /// queue before their next /do_matching poll resolves to an AI battle.
+ /// Defaults to 15 — matches the prod 4s pre-AIBattleStart pause plus a comfortable
+ /// polling cycle.
+ ///
+ public int RankBattleAiFallbackThresholdSeconds { get; set; } = 15;
+
+ public static MatchingConfig ShippedDefaults() => new();
+}
diff --git a/SVSim.UnitTests/Database/Config/MatchingConfigTests.cs b/SVSim.UnitTests/Database/Config/MatchingConfigTests.cs
new file mode 100644
index 0000000..2a1a1d3
--- /dev/null
+++ b/SVSim.UnitTests/Database/Config/MatchingConfigTests.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using NUnit.Framework;
+using SVSim.Database.Models.Config;
+
+namespace SVSim.UnitTests.Database.Config;
+
+[TestFixture]
+public class MatchingConfigTests
+{
+ [Test]
+ public void Default_threshold_is_15_seconds()
+ {
+ var cfg = new MatchingConfig();
+ Assert.That(cfg.RankBattleAiFallbackThresholdSeconds, Is.EqualTo(15));
+ }
+
+ [Test]
+ public void ShippedDefaults_returns_a_new_instance_with_default_threshold()
+ {
+ var cfg = MatchingConfig.ShippedDefaults();
+ Assert.That(cfg.RankBattleAiFallbackThresholdSeconds, Is.EqualTo(15));
+ }
+
+ [Test]
+ public void Has_ConfigSection_attribute_with_name_Matching()
+ {
+ var attr = typeof(MatchingConfig)
+ .GetCustomAttributes(typeof(ConfigSectionAttribute), false)
+ .Cast()
+ .FirstOrDefault();
+ Assert.That(attr, Is.Not.Null);
+ Assert.That(attr!.Name, Is.EqualTo("Matching"));
+ }
+}
diff --git a/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs b/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs
index c926b2a..7dd3e3b 100644
--- a/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs
+++ b/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs
@@ -25,13 +25,13 @@ public class GameConfigurationJsonbTests
var rows = await db.GameConfigs.AsNoTracking().ToListAsync();
var byName = rows.ToDictionary(r => r.SectionName);
- // One row per [ConfigSection]-marked POCO (11 sections today: Player, DefaultGrants,
+ // One row per [ConfigSection]-marked POCO (12 sections today: Player, DefaultGrants,
// DefaultLoadout, Challenge, Rotation, PackRates, MyRotationSchedule, Story, ResourceConfig,
- // Freeplay, ArenaTwoPick).
+ // Freeplay, ArenaTwoPick, Matching).
Assert.That(byName.Keys, Is.EquivalentTo(new[]
{
"Player", "DefaultGrants", "DefaultLoadout", "Challenge", "Rotation", "PackRates",
- "MyRotationSchedule", "Story", "ResourceConfig", "Freeplay", "ArenaTwoPick",
+ "MyRotationSchedule", "Story", "ResourceConfig", "Freeplay", "ArenaTwoPick", "Matching",
}));
var resources = JsonSerializer.Deserialize(byName["ResourceConfig"].ValueJson)!;