diff --git a/src/java/mmm/materials/MAlloyRecipe.java b/src/java/mmm/materials/MAlloyRecipe.java
new file mode 100644
index 0000000..72a0c97
--- /dev/null
+++ b/src/java/mmm/materials/MAlloyRecipe.java
@@ -0,0 +1,43 @@
+package mmm.materials;
+
+
+import net.minecraft.client.resources.I18n;
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fml.relauncher.Side;
+import net.minecraftforge.fml.relauncher.SideOnly;
+
+
+
+public class MAlloyRecipe
+{
+
+	public static final int MAX_ALLOY_INPUTS = 6;
+
+	public final String name;
+	public final int burnTime;
+	public final float xp;
+	public final ItemStack output;
+	public final ItemStack[] inputs;
+
+
+	MAlloyRecipe( final String name , final int burnTime , final float xp , final ItemStack output ,
+			final ItemStack[] inputs )
+	{
+		if ( inputs.length < 1 || inputs.length > MAlloyRecipe.MAX_ALLOY_INPUTS ) {
+			throw new IllegalArgumentException( "invalid alloy recipe" );
+		}
+		this.name = name;
+		this.burnTime = burnTime;
+		this.xp = xp;
+		this.output = output;
+		this.inputs = inputs;
+	}
+
+
+	@SideOnly( Side.CLIENT )
+	public String getLocalizedName( )
+	{
+		return I18n.format( this.name );
+	}
+
+}
diff --git a/src/java/mmm/materials/MAlloyRecipesRegistry.java b/src/java/mmm/materials/MAlloyRecipesRegistry.java
new file mode 100644
index 0000000..5cc10fa
--- /dev/null
+++ b/src/java/mmm/materials/MAlloyRecipesRegistry.java
@@ -0,0 +1,82 @@
+package mmm.materials;
+
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fml.relauncher.Side;
+import net.minecraftforge.fml.relauncher.SideOnly;
+
+
+
+public enum MAlloyRecipesRegistry {
+	INSTANCE;
+
+	private final HashMap< String , MAlloyRecipe > recipes = new HashMap<>( );
+
+
+	public void addRecipe( final int burnTime , final float xp , final Item output , final Object... params )
+	{
+		this.addRecipe( output.getUnlocalizedName( ) , burnTime , xp , new ItemStack( output ) , params );
+	}
+
+
+	public void addRecipe( final int burnTime , final float xp , final ItemStack output , final Object... params )
+	{
+		this.addRecipe( output.getUnlocalizedName( ) , burnTime , xp , output , params );
+	}
+
+
+	public void addRecipe( final String name , final int burnTime , final float xp , final Item output ,
+			final Object... params )
+	{
+		this.addRecipe( name , burnTime , xp , new ItemStack( output ) , params );
+	}
+
+
+	public void addRecipe( final String name , final int burnTime , final float xp , final ItemStack output ,
+			final Object... params )
+	{
+		final int nParams = params.length;
+		final ItemStack[] inputs = new ItemStack[ nParams ];
+		for ( int i = 0 ; i < nParams ; i++ ) {
+			final Object param = params[ i ];
+			if ( param instanceof ItemStack ) {
+				inputs[ i ] = (ItemStack) param;
+			} else if ( param instanceof Item ) {
+				inputs[ i ] = new ItemStack( (Item) param );
+			} else {
+				throw new IllegalArgumentException( "invalid alloy recipe input type" );
+			}
+		}
+		this.addRecipe( name , burnTime , xp , output , inputs );
+	}
+
+
+	public void addRecipe( final String name , final int burnTime , final float xp , final ItemStack output ,
+			final ItemStack[] inputs )
+	{
+		if ( this.recipes.containsKey( name ) ) {
+			throw new IllegalArgumentException( "duplicate alloy recipe '" + name + "'" );
+		}
+		final MAlloyRecipe recipe = new MAlloyRecipe( name , burnTime , xp , output , inputs );
+		this.recipes.put( name , recipe );
+	}
+
+
+	public MAlloyRecipe getRecipe( final String name )
+	{
+		return this.recipes.get( name );
+	}
+
+
+	@SideOnly( Side.CLIENT )
+	public ArrayList< MAlloyRecipe > getSortedRecipes( )
+	{
+		final ArrayList< MAlloyRecipe > output = new ArrayList<>( this.recipes.values( ) );
+		output.sort( ( r1 , r2 ) -> r1.getLocalizedName( ).compareTo( r2.getLocalizedName( ) ) );
+		return output;
+	}
+}