diff --git a/cashtab/src/components/Etokens/Token/index.js b/cashtab/src/components/Etokens/Token/index.js
--- a/cashtab/src/components/Etokens/Token/index.js
+++ b/cashtab/src/components/Etokens/Token/index.js
@@ -42,6 +42,7 @@
     getNftParentFanTxTargetOutputs,
     getNft,
     getNftChildSendTargetOutputs,
+    getAgoraAdFuelSats,
 } from 'slpv1';
 import { sendXec } from 'transactions';
 import {
@@ -907,10 +908,6 @@
     };
 
     const listNft = async () => {
-        // To guarantee we have no utxo conflicts while sending a chain of 2 txs
-        // We ensure that the target output of the ad setup tx will include enough XEC
-        // to cover the offer tx
-        const AD_SETUP_SATOSHIS = 3000;
         const listPriceSatoshis =
             selectedCurrency === appConfig.ticker
                 ? toSatoshis(formData.nftListPrice)
@@ -968,6 +965,32 @@
         const agoraAdScript = agoraOneshot.adScript();
         const agoraAdP2sh = Script.p2sh(shaRmd160(agoraAdScript.bytecode));
 
+        // We need to calculate the fee of the offer tx before we build the
+        // "ad prep" tx
+
+        // Determine the offerTx parameters before building txs, so we can
+        // accurately calculate its fee
+        const agoraScript = agoraOneshot.script();
+        const agoraP2sh = Script.p2sh(shaRmd160(agoraScript.bytecode));
+
+        const offerTargetOutputs = [
+            {
+                value: 0,
+                script: slpSend(tokenId, SLP_NFT1_CHILD, [1]),
+            },
+            { value: appConfig.dustSats, script: agoraP2sh },
+        ];
+        const offerTxFuelSats = getAgoraAdFuelSats(
+            agoraAdScript,
+            AgoraOneshotAdSignatory(sellerSk),
+            offerTargetOutputs,
+            satsPerKb,
+        );
+
+        // So, the ad prep tx must include an output with an input that covers this fee
+        // This will be dust + fee
+        const adFuelOutputSats = appConfig.dustSats + offerTxFuelSats;
+
         // Input needs to be the child NFT utxo with appropriate signData
         // Get the NFT utxo from Cashtab wallet
         const [thisNftUtxo] = getNft(tokenId, wallet.state.slpUtxos);
@@ -992,7 +1015,7 @@
                 value: 0,
                 script: slpSend(tokenId, SLP_NFT1_CHILD, [1]),
             },
-            { value: AD_SETUP_SATOSHIS, script: agoraAdP2sh },
+            { value: adFuelOutputSats, script: agoraAdP2sh },
         ];
 
         // Broadcast the ad setup tx
@@ -1029,10 +1052,6 @@
             return;
         }
 
-        // Seller finishes offer setup + sends NFT to the advertised P2SH
-        const agoraScript = agoraOneshot.script();
-        const agoraP2sh = Script.p2sh(shaRmd160(agoraScript.bytecode));
-
         const offerInputs = [
             // The actual NFT
             {
@@ -1044,20 +1063,13 @@
                         outIdx: 1,
                     },
                     signData: {
-                        value: AD_SETUP_SATOSHIS,
+                        value: adFuelOutputSats,
                         redeemScript: agoraAdScript,
                     },
                 },
                 signatory: AgoraOneshotAdSignatory(sellerSk),
             },
         ];
-        const offerTargetOutputs = [
-            {
-                value: 0,
-                script: slpSend(tokenId, SLP_NFT1_CHILD, [1]),
-            },
-            { value: appConfig.dustSats, script: agoraP2sh },
-        ];
 
         let offerTxid;
         try {
diff --git a/cashtab/src/components/Etokens/__tests__/TokenActions.test.js b/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
--- a/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
+++ b/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
@@ -660,9 +660,9 @@
 
         // NFT ad prep
         const adPrepHex =
-            '0200000002268322a2a8e67fe9efdaf15c9eb7397fb640ae32d8a245c2933f9eb967ff9b5d010000006441645e5d4141c2e9207f5a743a0f672f0710a352347df32954c3a24a19c64e8d9b95007335bf5491d4f69eac89a389fb1cbc3b868f3342ac38a7e23b5c8976c72e4121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffffef76d01776229a95c45696cf68f2f98c8332d0c53e3f24e73fd9c6deaf7926180300000064418a094d8cccbff0073e0b97fb84dfb849766545cbd4a764ad1d68b9a8ca5822feea93833c4df54fc5d7b52a73f0ba0fdf987a78f7481a5c55d351472f152931d04121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffff030000000000000000376a04534c500001410453454e44205d9bff67b99e3f93c245a2d832ae40b67f39b79e5cf1daefe97fe6a8a2228326080000000000000001b80b00000000000017a91407d2b0e6ec7b96cbfbe4a7d54e28d28fbcf65e4087f2290f00000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac00000000';
+            '0200000002268322a2a8e67fe9efdaf15c9eb7397fb640ae32d8a245c2933f9eb967ff9b5d010000006441e4365d350b1dfee55e60cc2600ba094ed0e05c1d6a297bd3fe3f0721b88d9ec09b7d114cf0aab08a3b264153858f1a48f839f3639a8a8f9b11214038080cb9e34121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffffef76d01776229a95c45696cf68f2f98c8332d0c53e3f24e73fd9c6deaf7926180300000064411e9913b28017832fa38944675eb8815411fd210f9dfc8f0aa806bed055f52b6592488fdd1f9be942c19dcb98d7ddd7c55bc8b1233a64ad3dfa1c65eebbd48f254121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffff030000000000000000376a04534c500001410453454e44205d9bff67b99e3f93c245a2d832ae40b67f39b79e5cf1daefe97fe6a8a22283260800000000000000019a0400000000000017a91407d2b0e6ec7b96cbfbe4a7d54e28d28fbcf65e408710310f00000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac00000000';
         const adPrepTxid =
-            '6543a807c83363e5d43587dcd15239b9c82a485e2b1ebbd730e82a0f0db4385f';
+            '7b4f2b1cf9716ead03f91910bd0c08956c381987e1cb3cd9f9b4d555a7b9ba25';
 
         mockedChronik.setMock('broadcastTx', {
             input: adPrepHex,
@@ -671,9 +671,9 @@
 
         // NFT ad list
         const adListHex =
-            '02000000015f38b40d0f2ae830d7bb1e2b5e482ac8b93952d1dc8735d4e56333c807a8436501000000a70441475230074f4e4553484f54416c8c3c5fb1d038dcce630a1504a04c2333c50fddfbb35b37c01ad4e89f0e2aae103703f2a2cf94cef51cbeff6c442feb961f4ec2fc948d59dc61b6ee83b3f262414c56222b50fe00000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac7521031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dad074f4e4553484f5488044147523087ffffffff030000000000000000376a04534c500001410453454e44205d9bff67b99e3f93c245a2d832ae40b67f39b79e5cf1daefe97fe6a8a2228326080000000000000001220200000000000017a914729833ae294590bbcf28bfbb9ad54c01b6cdb62887da060000000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac00000000';
+            '020000000125bab9a755d5b4f9d93ccbe18719386c95080cbd1019f903ad6e71f91c2b4f7b01000000a70441475230074f4e4553484f544106bd7c3cc4f6aca45a7f97644b8cb5e745dee224246f38605171e8f9e0d6e036af3ea4853b08e1baa92e091bd0ceabf83d4a246e07e6b0b008a3e091b111f22a414c56222b50fe00000000001976a91400549451e5c22b18686cacdf34dce649e5ec3be288ac7521031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dad074f4e4553484f5488044147523087ffffffff020000000000000000376a04534c500001410453454e44205d9bff67b99e3f93c245a2d832ae40b67f39b79e5cf1daefe97fe6a8a2228326080000000000000001220200000000000017a914729833ae294590bbcf28bfbb9ad54c01b6cdb6288700000000';
         const adListTxid =
-            'd732c995d4071786a689dff175c60982c57a14a3e2f6362ee0418fba6a5eca7f';
+            '97cf0fed5062419ad456f22457cfeb3b15909f1de2350be48c53b24944e0de89';
 
         mockedChronik.setMock('broadcastTx', {
             input: adListHex,
diff --git a/cashtab/src/slpv1/__tests__/index.test.js b/cashtab/src/slpv1/__tests__/index.test.js
--- a/cashtab/src/slpv1/__tests__/index.test.js
+++ b/cashtab/src/slpv1/__tests__/index.test.js
@@ -20,11 +20,49 @@
     getNft,
     getNftChildSendTargetOutputs,
     isTokenDustChangeOutput,
+    getAgoraAdFuelSats,
 } from 'slpv1';
 import vectors from '../fixtures/vectors';
-import { SEND_DESTINATION_ADDRESS } from '../fixtures/vectors';
+import { SEND_DESTINATION_ADDRESS, MOCK_TOKEN_ID } from '../fixtures/vectors';
+import { AgoraOneshot, AgoraOneshotAdSignatory } from 'ecash-agora';
+import {
+    initWasm,
+    slpSend,
+    SLP_NFT1_CHILD,
+    shaRmd160,
+    Script,
+    fromHex,
+} from 'ecash-lib';
+import appConfig from 'config/app';
+
+const MOCK_WALLET_HASH = fromHex('12'.repeat(20));
+const MOCK_PK = fromHex(
+    '03000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+);
 
 describe('slpv1 methods', () => {
+    let MOCK_AGORA_P2SH, MOCK_ONESHOT;
+    beforeAll(async () => {
+        // Initialize web assembly
+        await initWasm();
+
+        MOCK_ONESHOT = new AgoraOneshot({
+            enforcedOutputs: [
+                {
+                    value: 0,
+                    script: slpSend(MOCK_TOKEN_ID, SLP_NFT1_CHILD, [0, 1]),
+                },
+                {
+                    value: 10000, // list price satoshis
+                    script: Script.p2pkh(MOCK_WALLET_HASH),
+                },
+            ],
+            cancelPk: MOCK_PK,
+        });
+        MOCK_AGORA_P2SH = Script.p2sh(
+            shaRmd160(MOCK_ONESHOT.script().bytecode),
+        );
+    });
     describe('Generating etoken genesis tx target outputs', () => {
         const { expectedReturns, expectedErrors } =
             vectors.getSlpGenesisTargetOutput;
@@ -363,4 +401,46 @@
             });
         });
     });
+    describe.only('getAgoraAdFuelSats correctly determines one-input fee for an agora offer tx', () => {
+        const MOCK_WALLET_SK = fromHex('33'.repeat(32));
+        const SATS_PER_KB_MIN = 1000;
+        const SATS_PER_KB_ALT = 2000;
+
+        // Note: for NFT listings, this is more or less constant, at least in Cashtab
+        // maybe you could have a case where sendAmounts array is not [1], mb you have a weird
+        // NFT with "change" ... will not see this in Cashtab
+        // So, arguably this function could be a constant. However, we will extend to support
+        // partial agora offers, and we may change how these offers are made in the future
+        const mockOfferOutputs = [
+            {
+                value: 0,
+                script: slpSend(MOCK_TOKEN_ID, SLP_NFT1_CHILD, [1]),
+            },
+            {
+                value: appConfig.dustSats,
+                script: MOCK_AGORA_P2SH,
+            },
+        ];
+
+        it(`getAgoraAdFuelSats for minimum eCash fee`, () => {
+            expect(
+                getAgoraAdFuelSats(
+                    MOCK_ONESHOT.adScript(),
+                    AgoraOneshotAdSignatory(MOCK_WALLET_SK),
+                    mockOfferOutputs,
+                    SATS_PER_KB_MIN,
+                ),
+            ).toEqual(314);
+        });
+        it(`getAgoraAdFuelSats for a different fee level`, () => {
+            expect(
+                getAgoraAdFuelSats(
+                    MOCK_ONESHOT.adScript(),
+                    AgoraOneshotAdSignatory(MOCK_WALLET_SK),
+                    mockOfferOutputs,
+                    SATS_PER_KB_ALT,
+                ),
+            ).toEqual(628);
+        });
+    });
 });
diff --git a/cashtab/src/slpv1/index.js b/cashtab/src/slpv1/index.js
--- a/cashtab/src/slpv1/index.js
+++ b/cashtab/src/slpv1/index.js
@@ -4,7 +4,14 @@
 
 import appConfig from 'config/app';
 import { undecimalizeTokenAmount, decimalizeTokenAmount } from 'wallet';
-import { Script, slpGenesis, slpSend, slpMint } from 'ecash-lib';
+import {
+    Script,
+    slpGenesis,
+    slpSend,
+    slpMint,
+    TxBuilder,
+    EccDummy,
+} from 'ecash-lib';
 
 // Constants for SLP 1 token types as returned by chronik-client
 export const SLP_1_PROTOCOL_NUMBER = 1;
@@ -13,6 +20,9 @@
 // 0xffffffffffffffff
 export const MAX_MINT_AMOUNT_TOKEN_SATOSHIS = '18446744073709551615';
 
+const DUMMY_TXID =
+    '1111111111111111111111111111111111111111111111111111111111111111';
+
 // Cashtab spec
 // This is how Cashtab defines a token utxo to be received
 // by the wallet broadcasting this transaction.
@@ -611,3 +621,52 @@
         targetOutput.value === appConfig.dustSats
     );
 };
+
+/**
+ * For ecash-agora SLP1 listings txs, an "ad setup tx" is required before
+ * we can actually broadcast the offer
+ *
+ * We want to minimize the amount of XEC we need to make these two required txs
+ *
+ * So, we calculate the fee needed to send the 2nd tx (the offer tx)
+ * We will then use this fee to size the output of the first tx to exactly
+ * cover the 2nd tx
+ */
+export const getAgoraAdFuelSats = (
+    redeemScript,
+    signatory,
+    offerOutputs,
+    satsPerKb,
+) => {
+    // First, get the size of the listing tx
+    const dummyOfferTx = new TxBuilder({
+        inputs: [
+            {
+                input: {
+                    prevOut: {
+                        // Use a placeholder 32-byte txid
+                        txid: DUMMY_TXID,
+                        // The outIdx will always be 1 in Cashtab
+                        // In practice, this does not impact the tx size calculation
+                        outIdx: 1,
+                    },
+                    signData: {
+                        // Arbitrary value that we know will cover the fee for this tx,
+                        // which will always have only one input in Cashtab
+                        value: 10000,
+                        redeemScript,
+                    },
+                },
+                signatory,
+            },
+        ],
+        outputs: offerOutputs,
+    });
+    const measureTx = dummyOfferTx.sign(new EccDummy());
+
+    const dummyOfferTxSats = Math.ceil(
+        (measureTx.serSize() * satsPerKb) / 1000,
+    );
+
+    return dummyOfferTxSats;
+};
diff --git a/cashtab/src/transactions/index.js b/cashtab/src/transactions/index.js
--- a/cashtab/src/transactions/index.js
+++ b/cashtab/src/transactions/index.js
@@ -185,6 +185,7 @@
         // Otherwise, broadcast the tx
         const txSer = tx.ser();
         const hex = toHex(txSer);
+        console.log(`hex`, hex);
         // Will throw error on node failing to broadcast tx
         // e.g. 'txn-mempool-conflict (code 18)'
         const response = await chronik.broadcastTx(hex, isBurn);