diff --git a/assets/actions.go b/assets/actions.go
new file mode 100644
index 000000000..109da8ad5
--- /dev/null
+++ b/assets/actions.go
@@ -0,0 +1,463 @@
+package assets
+
+import (
+	"context"
+	"crypto/rand"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/lightninglabs/loop/fsm"
+	"github.com/lightninglabs/loop/swapserverrpc"
+	"github.com/lightninglabs/taproot-assets/address"
+	"github.com/lightninglabs/taproot-assets/taprpc"
+	"github.com/lightningnetwork/lnd/chainntnfs"
+	"github.com/lightningnetwork/lnd/lnrpc"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+// InitSwapOutContext is the initial context for the InitSwapOut state.
+type InitSwapOutContext struct {
+	// Amount is the amount of the swap.
+	Amount uint64
+	// AssetId is the id of the asset we are swapping.
+	AssetId []byte
+	// BlockheightHint is the hint for the current block height.
+	BlockHeightHint uint32
+}
+
+// InitSwapOut is the first state of the swap out FSM. It is responsible for
+// creating a new swap out and prepay invoice.
+func (o *OutFSM) InitSwapOut(ctx context.Context,
+	initCtx fsm.EventContext) fsm.EventType {
+
+	// We expect the event context to be of type *InstantOutContext.
+	req, ok := initCtx.(*InitSwapOutContext)
+	if !ok {
+		o.Errorf("expected InstantOutContext, got %T", initCtx)
+		return o.HandleError(fsm.ErrInvalidContextType)
+	}
+
+	// Create a new key for the swap.
+	clientKeyDesc, err := o.cfg.Wallet.DeriveNextKey(
+		ctx, AssetKeyFamily,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	// Request the asset out.
+	assetOutRes, err := o.cfg.AssetClient.RequestAssetLoopOut(
+		ctx, &swapserverrpc.RequestAssetLoopOutRequest{
+			Amount:         req.Amount,
+			RequestedAsset: req.AssetId,
+			ReceiverKey:    clientKeyDesc.PubKey.SerializeCompressed(),
+		},
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	// Create the swap hash from the response.
+	swapHash, err := lntypes.MakeHash(assetOutRes.SwapHash)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	// Parse the server pubkey.
+	senderPubkey, err := btcec.ParsePubKey(assetOutRes.SenderPubkey)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	// With our params, we'll now create the swap out.
+	swapOut := NewSwapOut(
+		swapHash, req.Amount,
+		req.AssetId, clientKeyDesc, senderPubkey,
+		uint32(assetOutRes.Expiry), req.BlockHeightHint,
+	)
+	o.SwapOut = swapOut
+	o.PrepayInvoice = assetOutRes.PrepayInvoice
+
+	err = o.cfg.Store.CreateAssetSwapOut(ctx, o.SwapOut)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	return onAssetOutInit
+}
+
+// PayPrepay is the state where we try to pay the prepay invoice.
+func (o *OutFSM) PayPrepay(ctx context.Context,
+	_ fsm.EventContext) fsm.EventType {
+
+	trackChan, errChan, err := o.cfg.TapdClient.SendPayment(
+		ctx, o.PrepayInvoice, o.SwapOut.AssetID,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	go func() {
+		for {
+			select {
+			case result := <-trackChan:
+				if result.GetAcceptedSellOrder() != nil {
+					o.Debugf("accepted prepay sell order")
+				}
+				if payRes := result.GetPaymentResult(); payRes != nil {
+					if payRes.GetFailureReason() !=
+						lnrpc.PaymentFailureReason_FAILURE_REASON_NONE {
+						o.Errorf("payment failed: %v", payRes.FailureReason)
+						err = o.SendEvent(ctx, fsm.OnError, nil)
+						if err != nil {
+							o.Errorf("unable to send event: %v", err)
+						}
+						return
+					}
+					if payRes.Status == lnrpc.Payment_SUCCEEDED {
+						o.Debugf("payment succeeded")
+						err := o.SendEvent(ctx, onPrepaySettled, nil)
+						if err != nil {
+							o.Errorf("unable to send event: %v", err)
+						}
+						return
+					}
+					if payRes.Status == lnrpc.Payment_IN_FLIGHT {
+						o.Debugf("payment in flight")
+					}
+				}
+
+			case err := <-errChan:
+				o.Errorf("payment error: %v", err)
+				err = o.SendEvent(ctx, fsm.OnError, nil)
+				if err != nil {
+					o.Errorf("unable to send event: %v", err)
+				}
+				return
+
+			case <-ctx.Done():
+				return
+			}
+		}
+	}()
+
+	return fsm.NoOp
+}
+
+// FetchProof is the state where we fetch the proof.
+func (o *OutFSM) FetchProof(ctx context.Context,
+	_ fsm.EventContext) fsm.EventType {
+
+	// Fetch the proof from the server.
+	proofRes, err := o.cfg.AssetClient.PollAssetLoopOutProof(
+		ctx, &swapserverrpc.PollAssetLoopOutProofRequest{
+			SwapHash: o.SwapOut.SwapHash[:],
+		},
+	)
+	// If we have an error, we'll wait for the next block and try again.
+	if err != nil {
+		return onWaitForBlock
+	}
+
+	// We'll now import the proof into the asset client.
+	_, err = o.cfg.TapdClient.ImportProofFile(
+		ctx, proofRes.RawProofFile,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	o.SwapOut.RawHtlcProof = proofRes.RawProofFile
+
+	// We'll now save the proof in the database.
+	err = o.cfg.Store.UpdateAssetSwapOutProof(
+		ctx, o.SwapOut.SwapHash, proofRes.RawProofFile,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	return onProofReceived
+}
+
+func (o *OutFSM) waitForBlock(ctx context.Context,
+	_ fsm.EventContext) fsm.EventType {
+
+	blockHeight := o.cfg.BlockHeightSubscriber.GetBlockHeight()
+
+	cb := func() {
+		err := o.SendEvent(ctx, onBlockReceived, nil)
+		if err != nil {
+			log.Errorf("Error sending block event %w", err)
+		}
+	}
+
+	subscriberId, err := getRandomHash()
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	alreadyPassed := o.cfg.BlockHeightSubscriber.SubscribeExpiry(
+		subscriberId, blockHeight+1, cb,
+	)
+	if alreadyPassed {
+		return onBlockReceived
+	}
+
+	return fsm.NoOp
+}
+
+// subscribeToHtlcTxConfirmed is the state where we subscribe to the htlc
+// transaction to wait for it to be confirmed.
+//
+// Todo(sputn1ck): handle rebroadcasting if it doesn't confirm.
+func (o *OutFSM) subscribeToHtlcTxConfirmed(ctx context.Context,
+	_ fsm.EventContext) fsm.EventType {
+
+	// First we'll get the htlc pkscript.
+	htlcPkScript, err := o.getHtlcPkscript()
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	o.Debugf("pkscript: %x", htlcPkScript)
+
+	txConfCtx, cancel := context.WithCancel(ctx)
+
+	confCallback := func(conf *chainntnfs.TxConfirmation, err error) {
+		if err != nil {
+			o.LastActionError = err
+			err = o.SendEvent(ctx, fsm.OnError, nil)
+			if err != nil {
+				log.Errorf("Error sending block event %w", err)
+			}
+		}
+		cancel()
+		err = o.SendEvent(ctx, onHtlcTxConfirmed, conf)
+		if err != nil {
+			log.Errorf("Error sending block event %w", err)
+		}
+	}
+
+	err = o.cfg.TxConfSubscriber.SubscribeTxConfirmation(
+		txConfCtx, o.SwapOut.SwapHash, nil,
+		htlcPkScript, defaultHtlcConfRequirement,
+		int32(o.SwapOut.InitiationHeight), confCallback,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	return fsm.NoOp
+}
+
+// sendSwapPayment is the state where we pay the swap invoice.
+func (o *OutFSM) sendSwapPayment(ctx context.Context,
+	event fsm.EventContext) fsm.EventType {
+
+	// If we have an EventContext with a confirmation, we'll save the
+	// confirmation height.
+	if event != nil {
+		if conf, ok := event.(*chainntnfs.TxConfirmation); ok {
+			outpoint, err := o.findPkScript(conf.Tx)
+			if err != nil {
+				return o.HandleError(err)
+			}
+			o.SwapOut.HtlcConfirmationHeight = conf.BlockHeight
+			o.SwapOut.HtlcOutPoint = outpoint
+
+			err = o.cfg.Store.UpdateAssetSwapHtlcOutpoint(
+				ctx, o.SwapOut.SwapHash,
+				outpoint, int32(conf.BlockHeight),
+			)
+			if err != nil {
+				o.Errorf(
+					"unable to update swap outpoint: %v",
+					err,
+				)
+			}
+		}
+	}
+
+	// Fetch the proof from the server.
+	buyRes, err := o.cfg.AssetClient.RequestAssetBuy(
+		ctx, &swapserverrpc.RequestAssetBuyRequest{
+			SwapHash: o.SwapOut.SwapHash[:],
+		},
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	// We'll also set the swap invoice.
+	o.SwapInvoice = buyRes.SwapInvoice
+
+	// If the htlc has been confirmed, we can now pay the swap invoice.
+	trackChan, errChan, err := o.cfg.TapdClient.SendPayment(
+		ctx, o.SwapInvoice, o.SwapOut.AssetID,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	go func() {
+		for {
+			select {
+			case result := <-trackChan:
+				if result.GetAcceptedSellOrder() != nil {
+					o.Debugf("swpa invoice accepted swap" +
+						"sell order")
+				}
+				if payRes := result.GetPaymentResult(); payRes != nil {
+					payStatus := payRes.GetStatus()
+					if payStatus == lnrpc.Payment_FAILED {
+						o.Errorf("payment failed: %v", payRes.FailureReason)
+						err = o.SendEvent(ctx, fsm.OnError, nil)
+						if err != nil {
+							o.Errorf("unable to send event: %v", err)
+						}
+						return
+					}
+					if payStatus == lnrpc.Payment_SUCCEEDED {
+						preimage, err := lntypes.MakePreimageFromStr(
+							payRes.PaymentPreimage,
+						)
+						if err != nil {
+							o.Errorf("unable to make preimage: %v", err)
+						}
+						o.SwapOut.SwapPreimage = preimage
+						err = o.cfg.Store.UpdateAssetSwapOutPreimage(
+							ctx, o.SwapOut.SwapHash,
+							preimage,
+						)
+						if err != nil {
+							o.Errorf(
+								"unable to update swap preimage: %v",
+								err,
+							)
+						}
+						err = o.SendEvent(ctx, onSwapPreimageReceived, nil)
+						if err != nil {
+							o.Errorf("unable to send event: %v", err)
+						}
+						return
+					}
+				}
+
+			case err := <-errChan:
+				o.Errorf("payment error: %v", err)
+				err = o.SendEvent(ctx, fsm.OnError, nil)
+				if err != nil {
+					o.Errorf("unable to send event: %v", err)
+				}
+				return
+
+			case <-ctx.Done():
+				return
+			}
+		}
+	}()
+
+	return fsm.NoOp
+}
+
+// publishSweepTx is the state where we publish the timeout transaction.
+func (o *OutFSM) publishSweepTx(ctx context.Context,
+	_ fsm.EventContext) fsm.EventType {
+
+	// Create the sweep address.
+	rpcSweepAddr, err := o.cfg.TapdClient.NewAddr(
+		ctx, &taprpc.NewAddrRequest{
+			AssetId: o.SwapOut.AssetID,
+			Amt:     o.SwapOut.Amount,
+		},
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	sweepAddr, err := address.DecodeAddress(
+		rpcSweepAddr.Encoded, o.cfg.AddrParams,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	// Publish and log the sweep transaction.
+	outpoint, pkScript, err := o.publishPreimageSweep(ctx, sweepAddr)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	o.SwapOut.SweepOutpoint = outpoint
+	o.SwapOut.SweepPkscript = pkScript
+
+	// We can now save the swap outpoint.
+	err = o.cfg.Store.UpdateAssetSwapOutSweepTx(
+		ctx, o.SwapOut.SwapHash, outpoint.Hash,
+		0, pkScript,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	return onHtlcSuccessSweep
+}
+
+// subscribeSweepConf is the state where we subscribe to the sweep transaction
+// confirmation.
+func (o *OutFSM) subscribeSweepConf(ctx context.Context,
+	_ fsm.EventContext) fsm.EventType {
+
+	// We'll now subscribe to the confirmation of the sweep transaction.
+	txConfCtx, cancel := context.WithCancel(ctx)
+
+	confCallback := func(conf *chainntnfs.TxConfirmation, err error) {
+		if err != nil {
+			o.LastActionError = err
+			err = o.SendEvent(ctx, fsm.OnError, nil)
+			if err != nil {
+				o.Errorf("Error sending conf event %w", err)
+			}
+		}
+		cancel()
+		err = o.SendEvent(ctx, onSweepTxConfirmed, conf)
+		if err != nil {
+			o.Errorf("Error sending conf event %w", err)
+		}
+	}
+
+	err := o.cfg.TxConfSubscriber.SubscribeTxConfirmation(
+		txConfCtx, o.SwapOut.SwapHash,
+		&o.SwapOut.SweepOutpoint.Hash, o.SwapOut.SweepPkscript,
+		defaultHtlcConfRequirement, int32(o.SwapOut.InitiationHeight),
+		confCallback,
+	)
+	if err != nil {
+		return o.HandleError(err)
+	}
+
+	return fsm.NoOp
+}
+
+// HandleError is a helper function that can be used by actions to handle
+// errors.
+func (o *OutFSM) HandleError(err error) fsm.EventType {
+	if o == nil {
+		log.Errorf("StateMachine error: %s", err)
+		return fsm.OnError
+	}
+	o.Errorf("StateMachine error: %s", err)
+	o.LastActionError = err
+	return fsm.OnError
+}
+
+// getRandomHash returns a random hash.
+func getRandomHash() (lntypes.Hash, error) {
+	var preimage lntypes.Preimage
+	_, err := rand.Read(preimage[:])
+	if err != nil {
+		return lntypes.Hash{}, err
+	}
+
+	return preimage.Hash(), nil
+}
diff --git a/assets/client.go b/assets/client.go
index 88c23efe5..49765249c 100644
--- a/assets/client.go
+++ b/assets/client.go
@@ -8,20 +8,43 @@ import (
 	"sync"
 	"time"
 
+	"bytes"
+
 	"github.com/btcsuite/btcd/btcutil"
+	"github.com/btcsuite/btcd/btcutil/psbt"
+	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/btcsuite/btcd/txscript"
+	"github.com/btcsuite/btcd/wire"
+	tap "github.com/lightninglabs/taproot-assets"
+	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightninglabs/taproot-assets/proof"
 	"github.com/lightninglabs/taproot-assets/rfqmath"
 	"github.com/lightninglabs/taproot-assets/rpcutils"
 	"github.com/lightninglabs/taproot-assets/tapcfg"
+	"github.com/lightninglabs/taproot-assets/tappsbt"
 	"github.com/lightninglabs/taproot-assets/taprpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
+	wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
 	"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
 	"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
 	"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
 	"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
+	"github.com/lightninglabs/taproot-assets/tapsend"
+	"github.com/lightninglabs/taproot-assets/universe"
+	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/lnrpc"
+	"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
+	"github.com/lightningnetwork/lnd/lntypes"
+	"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
+	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/macaroons"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/status"
 	"gopkg.in/macaroon.v2"
 )
 
@@ -65,7 +88,10 @@ type TapdClient struct {
 	tapchannelrpc.TaprootAssetChannelsClient
 	priceoraclerpc.PriceOracleClient
 	rfqrpc.RfqClient
+	wrpc.AssetWalletClient
+	mintrpc.MintClient
 	universerpc.UniverseClient
+	tapdevrpc.TapDevClient
 
 	cfg            *TapdConfig
 	assetNameCache map[string]string
@@ -313,3 +339,499 @@ func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) {
 
 	return conn, nil
 }
+
+// FundAndSignVpacket funds and signs a vpacket.
+func (t *TapdClient) FundAndSignVpacket(ctx context.Context,
+	vpkt *tappsbt.VPacket) (*tappsbt.VPacket, error) {
+
+	// Fund the packet.
+	var buf bytes.Buffer
+	err := vpkt.Serialize(&buf)
+	if err != nil {
+		return nil, err
+	}
+
+	fundResp, err := t.FundVirtualPsbt(
+		ctx, &assetwalletrpc.FundVirtualPsbtRequest{
+			Template: &assetwalletrpc.FundVirtualPsbtRequest_Psbt{
+				Psbt: buf.Bytes(),
+			},
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// Sign the packet.
+	signResp, err := t.SignVirtualPsbt(
+		ctx, &assetwalletrpc.SignVirtualPsbtRequest{
+			FundedPsbt: fundResp.FundedPsbt,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return tappsbt.NewFromRawBytes(
+		bytes.NewReader(signResp.SignedPsbt), false,
+	)
+}
+
+// addP2WPKHOutputToPsbt adds a normal bitcoin P2WPKH output to a psbt for the
+// given key and amount.
+func addP2WPKHOutputToPsbt(packet *psbt.Packet, keyDesc keychain.KeyDescriptor,
+	amount btcutil.Amount, params *chaincfg.Params) error {
+
+	derivation, _, _ := btcwallet.Bip32DerivationFromKeyDesc(
+		keyDesc, params.HDCoinType,
+	)
+
+	// Convert to Bitcoin address.
+	pubKeyBytes := keyDesc.PubKey.SerializeCompressed()
+	pubKeyHash := btcutil.Hash160(pubKeyBytes)
+	address, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, params)
+	if err != nil {
+		return err
+	}
+
+	// Generate the P2WPKH scriptPubKey.
+	scriptPubKey, err := txscript.PayToAddrScript(address)
+	if err != nil {
+		return err
+	}
+
+	// Add the output to the packet.
+	packet.UnsignedTx.AddTxOut(
+		wire.NewTxOut(int64(amount), scriptPubKey),
+	)
+
+	packet.Outputs = append(packet.Outputs, psbt.POutput{
+		Bip32Derivation: []*psbt.Bip32Derivation{
+			derivation,
+		},
+	})
+
+	return nil
+}
+
+// PrepareAndCommitVirtualPsbts prepares and commits virtual psbt to a BTC
+// template so that the underlying wallet can fund the transaction and add
+// the necessary additional input to pay for fees as well as a change output
+// if the change keydescriptor is not provided.
+func (t *TapdClient) PrepareAndCommitVirtualPsbts(ctx context.Context,
+	vpkt *tappsbt.VPacket, feeRateSatPerVByte chainfee.SatPerVByte,
+	changeKeyDesc *keychain.KeyDescriptor, params *chaincfg.Params) (
+	*psbt.Packet, []*tappsbt.VPacket, []*tappsbt.VPacket,
+	*assetwalletrpc.CommitVirtualPsbtsResponse, error) {
+
+	encodedVpkt, err := tappsbt.Encode(vpkt)
+	if err != nil {
+		return nil, nil, nil, nil, err
+	}
+
+	btcPkt, err := tapsend.PrepareAnchoringTemplate(
+		[]*tappsbt.VPacket{vpkt},
+	)
+	if err != nil {
+		return nil, nil, nil, nil, err
+	}
+
+	commitRequest := &assetwalletrpc.CommitVirtualPsbtsRequest{
+		Fees: &assetwalletrpc.CommitVirtualPsbtsRequest_SatPerVbyte{
+			SatPerVbyte: uint64(feeRateSatPerVByte),
+		},
+		AnchorChangeOutput: &assetwalletrpc.CommitVirtualPsbtsRequest_Add{ //nolint:lll
+			Add: true,
+		},
+		VirtualPsbts: [][]byte{
+			encodedVpkt,
+		},
+	}
+	if changeKeyDesc != nil {
+		err = addP2WPKHOutputToPsbt(
+			btcPkt, *changeKeyDesc, btcutil.Amount(1), params,
+		)
+		if err != nil {
+			return nil, nil, nil, nil, err
+		}
+		commitRequest.AnchorChangeOutput =
+			&assetwalletrpc.CommitVirtualPsbtsRequest_ExistingOutputIndex{ //nolint:lll
+				ExistingOutputIndex: 1,
+			}
+	} else {
+		commitRequest.AnchorChangeOutput =
+			&assetwalletrpc.CommitVirtualPsbtsRequest_Add{
+				Add: true,
+			}
+	}
+	var buf bytes.Buffer
+	err = btcPkt.Serialize(&buf)
+	if err != nil {
+		return nil, nil, nil, nil, err
+	}
+
+	commitRequest.AnchorPsbt = buf.Bytes()
+
+	commitResponse, err := t.AssetWalletClient.CommitVirtualPsbts(
+		ctx, commitRequest,
+	)
+	if err != nil {
+		return nil, nil, nil, nil, err
+	}
+
+	fundedPacket, err := psbt.NewFromRawBytes(
+		bytes.NewReader(commitResponse.AnchorPsbt), false,
+	)
+	if err != nil {
+		return nil, nil, nil, nil, err
+	}
+
+	activePackets := make(
+		[]*tappsbt.VPacket, len(commitResponse.VirtualPsbts),
+	)
+	for idx := range commitResponse.VirtualPsbts {
+		activePackets[idx], err = tappsbt.Decode(
+			commitResponse.VirtualPsbts[idx],
+		)
+		if err != nil {
+			return nil, nil, nil, nil, err
+		}
+	}
+
+	passivePackets := make(
+		[]*tappsbt.VPacket, len(commitResponse.PassiveAssetPsbts),
+	)
+	for idx := range commitResponse.PassiveAssetPsbts {
+		passivePackets[idx], err = tappsbt.Decode(
+			commitResponse.PassiveAssetPsbts[idx],
+		)
+		if err != nil {
+			return nil, nil, nil, nil, err
+		}
+	}
+
+	return fundedPacket, activePackets, passivePackets, commitResponse, nil
+}
+
+// LogAndPublish logs and publishes a psbt with the given active and passive
+// assets.
+func (t *TapdClient) LogAndPublish(ctx context.Context, btcPkt *psbt.Packet,
+	activeAssets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket,
+	commitResp *assetwalletrpc.CommitVirtualPsbtsResponse) (
+	*taprpc.SendAssetResponse, error) {
+
+	var buf bytes.Buffer
+	err := btcPkt.Serialize(&buf)
+	if err != nil {
+		return nil, err
+	}
+
+	request := &assetwalletrpc.PublishAndLogRequest{
+		AnchorPsbt:        buf.Bytes(),
+		VirtualPsbts:      make([][]byte, len(activeAssets)),
+		PassiveAssetPsbts: make([][]byte, len(passiveAssets)),
+		ChangeOutputIndex: commitResp.ChangeOutputIndex,
+		LndLockedUtxos:    commitResp.LndLockedUtxos,
+	}
+
+	for idx := range activeAssets {
+		request.VirtualPsbts[idx], err = tappsbt.Encode(
+			activeAssets[idx],
+		)
+		if err != nil {
+			return nil, err
+		}
+	}
+	for idx := range passiveAssets {
+		request.PassiveAssetPsbts[idx], err = tappsbt.Encode(
+			passiveAssets[idx],
+		)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	resp, err := t.PublishAndLogTransfer(ctx, request)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
+// ListAvailableAssets returns a list of available assets.
+func (t *TapdClient) ListAvailableAssets(ctx context.Context) (
+	[][]byte, error) {
+
+	balanceRes, err := t.ListBalances(ctx, &taprpc.ListBalancesRequest{
+		GroupBy: &taprpc.ListBalancesRequest_AssetId{
+			AssetId: true,
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	assets := make([][]byte, 0, len(balanceRes.AssetBalances))
+	for assetID := range balanceRes.AssetBalances {
+		asset, err := hex.DecodeString(assetID)
+		if err != nil {
+			return nil, err
+		}
+		assets = append(assets, asset)
+	}
+
+	return assets, nil
+}
+
+// GetAssetBalance checks the balance of an asset by its ID.
+func (t *TapdClient) GetAssetBalance(ctx context.Context, assetId []byte) (
+	uint64, error) {
+
+	// Check if we have enough funds to do the swap.
+	balanceResp, err := t.ListBalances(
+		ctx, &taprpc.ListBalancesRequest{
+			GroupBy: &taprpc.ListBalancesRequest_AssetId{
+				AssetId: true,
+			},
+			AssetFilter: assetId,
+		},
+	)
+	if err != nil {
+		return 0, err
+	}
+
+	// Check if we have enough funds to do the swap.
+	balance, ok := balanceResp.AssetBalances[hex.EncodeToString(
+		assetId,
+	)]
+	if !ok {
+		return 0, status.Error(codes.Internal, "internal error")
+	}
+
+	return balance.Balance, nil
+}
+
+// GetUnEncumberedAssetBalance returns the total balance of the given asset for
+// which the given client owns the script keys.
+func (t *TapdClient) GetUnEncumberedAssetBalance(ctx context.Context,
+	assetID []byte) (uint64, error) {
+
+	allAssets, err := t.ListAssets(ctx, &taprpc.ListAssetRequest{})
+	if err != nil {
+		return 0, err
+	}
+
+	var balance uint64
+	for _, a := range allAssets.Assets {
+		// Only count assets from the given asset ID.
+		if !bytes.Equal(a.AssetGenesis.AssetId, assetID) {
+			continue
+		}
+
+		// Non-local means we don't have the internal key to spend the
+		// asset.
+		if !a.ScriptKeyIsLocal {
+			continue
+		}
+
+		// If the asset is not declared known or has a script path, we
+		// can't spend it directly.
+		if !a.ScriptKeyDeclaredKnown || a.ScriptKeyHasScriptPath {
+			continue
+		}
+
+		balance += a.Amount
+	}
+
+	return balance, nil
+}
+
+// DeriveNewKeys derives a new internal and script key.
+func (t *TapdClient) DeriveNewKeys(ctx context.Context) (asset.ScriptKey,
+	keychain.KeyDescriptor, error) {
+
+	scriptKeyDesc, err := t.NextScriptKey(
+		ctx, &assetwalletrpc.NextScriptKeyRequest{
+			KeyFamily: uint32(asset.TaprootAssetsKeyFamily),
+		},
+	)
+	if err != nil {
+		return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
+	}
+
+	scriptKey, err := rpcutils.UnmarshalScriptKey(scriptKeyDesc.ScriptKey)
+	if err != nil {
+		return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
+	}
+
+	internalKeyDesc, err := t.NextInternalKey(
+		ctx, &assetwalletrpc.NextInternalKeyRequest{
+			KeyFamily: uint32(asset.TaprootAssetsKeyFamily),
+		},
+	)
+	if err != nil {
+		return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
+	}
+	internalKeyLnd, err := rpcutils.UnmarshalKeyDescriptor(
+		internalKeyDesc.InternalKey,
+	)
+	if err != nil {
+		return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
+	}
+
+	return *scriptKey, internalKeyLnd, nil
+}
+
+// ImportProofFile imports the proof file and returns the last proof.
+func (t *TapdClient) ImportProofFile(ctx context.Context, rawProofFile []byte) (
+	*proof.Proof, error) {
+
+	proofFile, err := proof.DecodeFile(rawProofFile)
+	if err != nil {
+		return nil, err
+	}
+
+	var lastProof *proof.Proof
+
+	for i := 0; i < proofFile.NumProofs(); i++ {
+		lastProof, err = proofFile.ProofAt(uint32(i))
+		if err != nil {
+			return nil, err
+		}
+
+		var proofBytes bytes.Buffer
+		err = lastProof.Encode(&proofBytes)
+		if err != nil {
+			return nil, err
+		}
+
+		asset := lastProof.Asset
+
+		proofType := universe.ProofTypeTransfer
+		if asset.IsGenesisAsset() {
+			proofType = universe.ProofTypeIssuance
+		}
+
+		uniID := universe.Identifier{
+			AssetID:   asset.ID(),
+			ProofType: proofType,
+		}
+		if asset.GroupKey != nil {
+			uniID.GroupKey = &asset.GroupKey.GroupPubKey
+		}
+
+		rpcUniID, err := tap.MarshalUniID(uniID)
+		if err != nil {
+			return nil, err
+		}
+
+		outpoint := &universerpc.Outpoint{
+			HashStr: lastProof.AnchorTx.TxHash().String(),
+			Index:   int32(lastProof.InclusionProof.OutputIndex),
+		}
+
+		scriptKey := lastProof.Asset.ScriptKey.PubKey
+		leafKey := &universerpc.AssetKey{
+			Outpoint: &universerpc.AssetKey_Op{
+				Op: outpoint,
+			},
+			ScriptKey: &universerpc.AssetKey_ScriptKeyBytes{
+				ScriptKeyBytes: scriptKey.SerializeCompressed(),
+			},
+		}
+
+		_, err = t.InsertProof(ctx, &universerpc.AssetProof{
+			Key: &universerpc.UniverseKey{
+				Id:      rpcUniID,
+				LeafKey: leafKey,
+			},
+			AssetLeaf: &universerpc.AssetLeaf{
+				Proof: proofBytes.Bytes(),
+			},
+		})
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return lastProof, nil
+}
+
+func (t *TapdClient) AddHoldInvoice(ctx context.Context, pHash lntypes.Hash,
+	assetId []byte, assetAmt uint64, memo string) (
+	*tapchannelrpc.AddInvoiceResponse, error) {
+
+	// Now we can create the swap invoice.
+	invoiceRes, err := t.AddInvoice(
+		ctx, &tapchannelrpc.AddInvoiceRequest{
+
+			// Todo(sputn1ck):if we have more than one peer, we'll need to
+			// specify one. This will likely be changed on the tapd front in
+			// the future.
+			PeerPubkey:  nil,
+			AssetId:     assetId,
+			AssetAmount: assetAmt,
+			InvoiceRequest: &lnrpc.Invoice{
+				Memo:  memo,
+				RHash: pHash[:],
+				// todo fix expiries
+				CltvExpiry: 144,
+				Expiry:     60,
+				Private:    true,
+			},
+			HodlInvoice: &tapchannelrpc.HodlInvoice{
+				PaymentHash: pHash[:],
+			},
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return invoiceRes, nil
+}
+
+func (t *TapdClient) SendPayment(ctx context.Context,
+	invoice string, assetId []byte) (chan *tapchannelrpc.SendPaymentResponse,
+	chan error, error) {
+
+	req := &routerrpc.SendPaymentRequest{
+		PaymentRequest: invoice,
+	}
+
+	sendReq := &tapchannelrpc.SendPaymentRequest{
+		AssetId:        assetId,
+		PaymentRequest: req,
+	}
+
+	sendResp, err := t.TaprootAssetChannelsClient.SendPayment(
+		ctx, sendReq,
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	sendRespChan := make(chan *tapchannelrpc.SendPaymentResponse)
+	errChan := make(chan error)
+	go func() {
+		defer close(sendRespChan)
+		defer close(errChan)
+		for {
+			select {
+			case <-ctx.Done():
+				errChan <- ctx.Err()
+				return
+			default:
+				res, err := sendResp.Recv()
+				if err != nil {
+					errChan <- err
+					return
+				}
+				sendRespChan <- res
+			}
+		}
+	}()
+
+	return sendRespChan, errChan, nil
+}
diff --git a/assets/htlc/script.go b/assets/htlc/script.go
new file mode 100644
index 000000000..842bbc66d
--- /dev/null
+++ b/assets/htlc/script.go
@@ -0,0 +1,88 @@
+package htlc
+
+import (
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/txscript"
+	"github.com/decred/dcrd/dcrec/secp256k1/v4"
+	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+// GenSuccessPathScript constructs an HtlcScript for the success payment path.
+func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey,
+	swapHash lntypes.Hash) ([]byte, error) {
+
+	builder := txscript.NewScriptBuilder()
+
+	builder.AddData(schnorr.SerializePubKey(receiverHtlcKey))
+	builder.AddOp(txscript.OP_CHECKSIGVERIFY)
+	builder.AddOp(txscript.OP_SIZE)
+	builder.AddInt64(32)
+	builder.AddOp(txscript.OP_EQUALVERIFY)
+	builder.AddOp(txscript.OP_HASH160)
+	builder.AddData(input.Ripemd160H(swapHash[:]))
+	builder.AddOp(txscript.OP_EQUALVERIFY)
+	builder.AddInt64(1)
+	builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
+
+	return builder.Script()
+}
+
+// GenTimeoutPathScript constructs an HtlcScript for the timeout payment path.
+func GenTimeoutPathScript(senderHtlcKey *btcec.PublicKey, csvExpiry int64) (
+	[]byte, error) {
+
+	builder := txscript.NewScriptBuilder()
+	builder.AddData(schnorr.SerializePubKey(senderHtlcKey))
+	builder.AddOp(txscript.OP_CHECKSIGVERIFY)
+	builder.AddInt64(csvExpiry)
+	builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
+	return builder.Script()
+}
+
+// GetOpTrueScript returns a script that always evaluates to true.
+func GetOpTrueScript() ([]byte, error) {
+	return txscript.NewScriptBuilder().AddOp(txscript.OP_TRUE).Script()
+}
+
+// CreateOpTrueLeaf creates a taproot leaf that always evaluates to true.
+func CreateOpTrueLeaf() (asset.ScriptKey, txscript.TapLeaf,
+	*txscript.IndexedTapScriptTree, *txscript.ControlBlock, error) {
+
+	// Create the taproot OP_TRUE script.
+	tapScript, err := GetOpTrueScript()
+	if err != nil {
+		return asset.ScriptKey{}, txscript.TapLeaf{}, nil, nil, err
+	}
+
+	tapLeaf := txscript.NewBaseTapLeaf(tapScript)
+	tree := txscript.AssembleTaprootScriptTree(tapLeaf)
+	rootHash := tree.RootNode.TapHash()
+	tapKey := txscript.ComputeTaprootOutputKey(asset.NUMSPubKey, rootHash[:])
+
+	merkleRootHash := tree.RootNode.TapHash()
+
+	controlBlock := &txscript.ControlBlock{
+		LeafVersion: txscript.BaseLeafVersion,
+		InternalKey: asset.NUMSPubKey,
+	}
+	tapScriptKey := asset.ScriptKey{
+		PubKey: tapKey,
+		TweakedScriptKey: &asset.TweakedScriptKey{
+			RawKey: keychain.KeyDescriptor{
+				PubKey: asset.NUMSPubKey,
+			},
+			Tweak: merkleRootHash[:],
+		},
+	}
+	if tapKey.SerializeCompressed()[0] ==
+		secp256k1.PubKeyFormatCompressedOdd {
+
+		controlBlock.OutputKeyYIsOdd = true
+	}
+
+	return tapScriptKey, tapLeaf, tree, controlBlock, nil
+}
diff --git a/assets/htlc/swapkit.go b/assets/htlc/swapkit.go
new file mode 100644
index 000000000..8d562b80e
--- /dev/null
+++ b/assets/htlc/swapkit.go
@@ -0,0 +1,457 @@
+package htlc
+
+import (
+	"context"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcutil/psbt"
+	"github.com/btcsuite/btcd/txscript"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/decred/dcrd/dcrec/secp256k1/v4"
+	"github.com/lightninglabs/lndclient"
+	"github.com/lightninglabs/taproot-assets/address"
+	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightninglabs/taproot-assets/commitment"
+	"github.com/lightninglabs/taproot-assets/proof"
+	"github.com/lightninglabs/taproot-assets/tappsbt"
+	"github.com/lightninglabs/taproot-assets/tapscript"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+// SwapKit holds information needed to facilitate an on-chain asset to offchain
+// bitcoin atomic swap. The keys within the struct are the public keys of the
+// sender and receiver that will be used to create the on-chain HTLC.
+type SwapKit struct {
+	// SenderPubKey is the public key of the sender for the joint key
+	// that will be used to create the HTLC.
+	SenderPubKey *btcec.PublicKey
+
+	// ReceiverPubKey is the public key of the receiver that will be used to
+	// create the HTLC.
+	ReceiverPubKey *btcec.PublicKey
+
+	// AssetID is the identifier of the asset that will be swapped.
+	AssetID []byte
+
+	// Amount is the amount of the asset that will be swapped. Note that
+	// we use btcutil.Amount here for simplicity, but the actual amount
+	// is in the asset's native unit.
+	Amount uint64
+
+	// SwapHash is the hash of the preimage in the  swap HTLC.
+	SwapHash lntypes.Hash
+
+	// CsvExpiry is the relative timelock in blocks for the swap.
+	CsvExpiry uint32
+}
+
+// GetSuccessScript returns the success path script of the swap HTLC.
+func (s *SwapKit) GetSuccessScript() ([]byte, error) {
+	return GenSuccessPathScript(s.ReceiverPubKey, s.SwapHash)
+}
+
+// GetTimeoutScript returns the timeout path script of the swap HTLC.
+func (s *SwapKit) GetTimeoutScript() ([]byte, error) {
+	return GenTimeoutPathScript(s.SenderPubKey, int64(s.CsvExpiry))
+}
+
+// GetAggregateKey returns the aggregate MuSig2 key used in the swap HTLC.
+func (s *SwapKit) GetAggregateKey() (*btcec.PublicKey, error) {
+	aggregateKey, err := input.MuSig2CombineKeys(
+		input.MuSig2Version100RC2,
+		[]*btcec.PublicKey{
+			s.SenderPubKey, s.ReceiverPubKey,
+		},
+		true,
+		&input.MuSig2Tweaks{},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return aggregateKey.PreTweakedKey, nil
+}
+
+// GetTimeOutLeaf returns the timeout leaf of the swap.
+func (s *SwapKit) GetTimeOutLeaf() (txscript.TapLeaf, error) {
+	timeoutScript, err := s.GetTimeoutScript()
+	if err != nil {
+		return txscript.TapLeaf{}, err
+	}
+
+	timeoutLeaf := txscript.NewBaseTapLeaf(timeoutScript)
+
+	return timeoutLeaf, nil
+}
+
+// GetSuccessLeaf returns the success leaf of the swap.
+func (s *SwapKit) GetSuccessLeaf() (txscript.TapLeaf, error) {
+	successScript, err := s.GetSuccessScript()
+	if err != nil {
+		return txscript.TapLeaf{}, err
+	}
+
+	successLeaf := txscript.NewBaseTapLeaf(successScript)
+
+	return successLeaf, nil
+}
+
+// GetSiblingPreimage returns the sibling preimage of the HTLC bitcoin top level
+// output.
+func (s *SwapKit) GetSiblingPreimage() (commitment.TapscriptPreimage, error) {
+	timeOutLeaf, err := s.GetTimeOutLeaf()
+	if err != nil {
+		return commitment.TapscriptPreimage{}, err
+	}
+
+	successLeaf, err := s.GetSuccessLeaf()
+	if err != nil {
+		return commitment.TapscriptPreimage{}, err
+	}
+
+	branch := txscript.NewTapBranch(timeOutLeaf, successLeaf)
+
+	siblingPreimage := commitment.NewPreimageFromBranch(branch)
+
+	return siblingPreimage, nil
+}
+
+// CreateHtlcVpkt creates the vpacket for the HTLC.
+func (s *SwapKit) CreateHtlcVpkt(addressParams *address.ChainParams) (
+	*tappsbt.VPacket, error) {
+
+	assetId := asset.ID{}
+	copy(assetId[:], s.AssetID)
+
+	btcInternalKey, err := s.GetAggregateKey()
+	if err != nil {
+		return nil, err
+	}
+
+	siblingPreimage, err := s.GetSiblingPreimage()
+	if err != nil {
+		return nil, err
+	}
+
+	tapScriptKey, _, _, _, err := CreateOpTrueLeaf()
+	if err != nil {
+		return nil, err
+	}
+
+	pkt := &tappsbt.VPacket{
+		Inputs: []*tappsbt.VInput{{
+			PrevID: asset.PrevID{
+				ID: assetId,
+			},
+		}},
+		Outputs:     make([]*tappsbt.VOutput, 0, 2),
+		ChainParams: addressParams,
+		Version:     tappsbt.V1,
+	}
+	pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{
+		Amount:            0,
+		Type:              tappsbt.TypeSplitRoot,
+		AnchorOutputIndex: 0,
+		ScriptKey:         asset.NUMSScriptKey,
+	})
+	pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{
+		// todo(sputn1ck) assetversion
+		AssetVersion:      asset.Version(1),
+		Amount:            s.Amount,
+		Interactive:       true,
+		AnchorOutputIndex: 1,
+		ScriptKey: asset.NewScriptKey(
+			tapScriptKey.PubKey,
+		),
+		AnchorOutputInternalKey:      btcInternalKey,
+		AnchorOutputTapscriptSibling: &siblingPreimage,
+	})
+
+	return pkt, nil
+}
+
+// GenTimeoutBtcControlBlock generates the control block for the timeout path of
+// the swap.
+func (s *SwapKit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) (
+	*txscript.ControlBlock, error) {
+
+	internalKey, err := s.GetAggregateKey()
+	if err != nil {
+		return nil, err
+	}
+
+	successLeaf, err := s.GetSuccessLeaf()
+	if err != nil {
+		return nil, err
+	}
+
+	successLeafHash := successLeaf.TapHash()
+
+	btcControlBlock := &txscript.ControlBlock{
+		InternalKey: internalKey,
+		LeafVersion: txscript.BaseLeafVersion,
+		InclusionProof: append(
+			successLeafHash[:], taprootAssetRoot...,
+		),
+	}
+
+	timeoutPathScript, err := s.GetTimeoutScript()
+	if err != nil {
+		return nil, err
+	}
+
+	rootHash := btcControlBlock.RootHash(timeoutPathScript)
+	tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
+	if tapKey.SerializeCompressed()[0] ==
+		secp256k1.PubKeyFormatCompressedOdd {
+
+		btcControlBlock.OutputKeyYIsOdd = true
+	}
+
+	return btcControlBlock, nil
+}
+
+// GenSuccessBtcControlBlock generates the control block for the timeout path of
+// the swap.
+func (s *SwapKit) GenSuccessBtcControlBlock(taprootAssetRoot []byte) (
+	*txscript.ControlBlock, error) {
+
+	internalKey, err := s.GetAggregateKey()
+	if err != nil {
+		return nil, err
+	}
+
+	timeOutLeaf, err := s.GetTimeOutLeaf()
+	if err != nil {
+		return nil, err
+	}
+
+	timeOutLeafHash := timeOutLeaf.TapHash()
+
+	btcControlBlock := &txscript.ControlBlock{
+		InternalKey: internalKey,
+		LeafVersion: txscript.BaseLeafVersion,
+		InclusionProof: append(
+			timeOutLeafHash[:], taprootAssetRoot...,
+		),
+	}
+
+	successPathScript, err := s.GetSuccessScript()
+	if err != nil {
+		return nil, err
+	}
+
+	rootHash := btcControlBlock.RootHash(successPathScript)
+	tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
+	if tapKey.SerializeCompressed()[0] ==
+		secp256k1.PubKeyFormatCompressedOdd {
+
+		btcControlBlock.OutputKeyYIsOdd = true
+	}
+
+	return btcControlBlock, nil
+}
+
+// GenTaprootAssetRootFromProof generates the taproot asset root from the proof
+// of the swap.
+func GenTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, error) {
+	assetCopy := proof.Asset.CopySpendTemplate()
+
+	version := commitment.TapCommitmentV2
+	assetCommitment, err := commitment.FromAssets(
+		&version, assetCopy,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	assetCommitment, err = commitment.TrimSplitWitnesses(
+		&version, assetCommitment,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	taprootAssetRoot := assetCommitment.TapscriptRoot(nil)
+
+	return taprootAssetRoot[:], nil
+}
+
+// GetPkScriptFromAsset returns the toplevel bitcoin script with the given
+// asset.
+func (s *SwapKit) GetPkScriptFromAsset(asset *asset.Asset) ([]byte, error) {
+	assetCopy := asset.CopySpendTemplate()
+
+	version := commitment.TapCommitmentV2
+	assetCommitment, err := commitment.FromAssets(
+		&version, assetCopy,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	assetCommitment, err = commitment.TrimSplitWitnesses(
+		&version, assetCommitment,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	siblingPreimage, err := s.GetSiblingPreimage()
+	if err != nil {
+		return nil, err
+	}
+
+	siblingHash, err := siblingPreimage.TapHash()
+	if err != nil {
+		return nil, err
+	}
+
+	btcInternalKey, err := s.GetAggregateKey()
+	if err != nil {
+		return nil, err
+	}
+
+	return tapscript.PayToAddrScript(
+		*btcInternalKey, siblingHash, *assetCommitment,
+	)
+}
+
+// CreatePreimageWitness creates a preimage witness for the swap.
+func (s *SwapKit) CreatePreimageWitness(ctx context.Context,
+	signer lndclient.SignerClient, htlcProof *proof.Proof,
+	sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator,
+	preimage lntypes.Preimage) (wire.TxWitness, error) {
+
+	assetTxOut := &wire.TxOut{
+		PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript,
+		Value:    sweepBtcPacket.Inputs[0].WitnessUtxo.Value,
+	}
+	feeTxOut := &wire.TxOut{
+		PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript,
+		Value:    sweepBtcPacket.Inputs[1].WitnessUtxo.Value,
+	}
+
+	sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1
+
+	successScript, err := s.GetSuccessScript()
+	if err != nil {
+		return nil, err
+	}
+
+	signDesc := &lndclient.SignDescriptor{
+		KeyDesc: keychain.KeyDescriptor{
+			KeyLocator: keyLocator,
+		},
+		SignMethod:    input.TaprootScriptSpendSignMethod,
+		WitnessScript: successScript,
+		Output:        assetTxOut,
+		InputIndex:    0,
+	}
+	sig, err := signer.SignOutputRaw(
+		ctx, sweepBtcPacket.UnsignedTx,
+		[]*lndclient.SignDescriptor{
+			signDesc,
+		},
+		[]*wire.TxOut{
+			assetTxOut, feeTxOut,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof)
+	if err != nil {
+		return nil, err
+	}
+
+	successControlBlock, err := s.GenSuccessBtcControlBlock(
+		taprootAssetRoot,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	controlBlockBytes, err := successControlBlock.ToBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	return wire.TxWitness{
+		preimage[:],
+		sig[0],
+		successScript,
+		controlBlockBytes,
+	}, nil
+}
+
+// CreateTimeoutWitness creates a timeout witness for the swap.
+func (s *SwapKit) CreateTimeoutWitness(ctx context.Context,
+	signer lndclient.SignerClient, htlcProof *proof.Proof,
+	sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator) (
+	wire.TxWitness, error) {
+
+	assetTxOut := &wire.TxOut{
+		PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript,
+		Value:    sweepBtcPacket.Inputs[0].WitnessUtxo.Value,
+	}
+	feeTxOut := &wire.TxOut{
+		PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript,
+		Value:    sweepBtcPacket.Inputs[1].WitnessUtxo.Value,
+	}
+
+	sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = s.CsvExpiry
+
+	timeoutScript, err := s.GetTimeoutScript()
+	if err != nil {
+		return nil, err
+	}
+
+	signDesc := &lndclient.SignDescriptor{
+		KeyDesc: keychain.KeyDescriptor{
+			KeyLocator: keyLocator,
+		},
+		SignMethod:    input.TaprootScriptSpendSignMethod,
+		WitnessScript: timeoutScript,
+		Output:        assetTxOut,
+		InputIndex:    0,
+	}
+	sig, err := signer.SignOutputRaw(
+		ctx, sweepBtcPacket.UnsignedTx,
+		[]*lndclient.SignDescriptor{
+			signDesc,
+		},
+		[]*wire.TxOut{
+			assetTxOut, feeTxOut,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof)
+	if err != nil {
+		return nil, err
+	}
+
+	timeoutControlBlock, err := s.GenTimeoutBtcControlBlock(
+		taprootAssetRoot,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	controlBlockBytes, err := timeoutControlBlock.ToBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	return wire.TxWitness{
+		sig[0],
+		timeoutScript,
+		controlBlockBytes,
+	}, nil
+}
diff --git a/assets/interfaces.go b/assets/interfaces.go
new file mode 100644
index 000000000..daf90a3c7
--- /dev/null
+++ b/assets/interfaces.go
@@ -0,0 +1,153 @@
+package assets
+
+import (
+	"context"
+
+	"github.com/btcsuite/btcd/btcutil"
+	"github.com/btcsuite/btcd/btcutil/psbt"
+	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/lndclient"
+	"github.com/lightninglabs/loop/fsm"
+	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightninglabs/taproot-assets/proof"
+	"github.com/lightninglabs/taproot-assets/tappsbt"
+	"github.com/lightninglabs/taproot-assets/taprpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
+	wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
+	"github.com/lightningnetwork/lnd/chainntnfs"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lntypes"
+	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
+)
+
+const (
+	// DefaultSwapCSVExpiry is the default expiry for a swap in blocks.
+	DefaultSwapCSVExpiry = int32(24)
+
+	defaultHtlcFeeConfTarget   = 3
+	defaultHtlcConfRequirement = 2
+
+	AssetKeyFamily = 696969
+)
+
+// TapdClient is an interface that groups the methods required to interact with
+// the taproot-assets server and the wallet.
+type AssetClient interface {
+	taprpc.TaprootAssetsClient
+	wrpc.AssetWalletClient
+	mintrpc.MintClient
+	universerpc.UniverseClient
+	tapdevrpc.TapDevClient
+
+	// FundAndSignVpacket funds ands signs a vpacket.
+	FundAndSignVpacket(ctx context.Context,
+		vpkt *tappsbt.VPacket) (*tappsbt.VPacket, error)
+
+	// PrepareAndCommitVirtualPsbts prepares and commits virtual psbts.
+	PrepareAndCommitVirtualPsbts(ctx context.Context,
+		vpkt *tappsbt.VPacket, feeRateSatPerKVByte chainfee.SatPerVByte,
+		changeKeyDesc *keychain.KeyDescriptor, params *chaincfg.Params) (
+		*psbt.Packet, []*tappsbt.VPacket, []*tappsbt.VPacket,
+		*assetwalletrpc.CommitVirtualPsbtsResponse, error)
+
+	// LogAndPublish logs and publishes the virtual psbts.
+	LogAndPublish(ctx context.Context, btcPkt *psbt.Packet,
+		activeAssets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket,
+		commitResp *wrpc.CommitVirtualPsbtsResponse) (*taprpc.SendAssetResponse,
+		error)
+
+	// GetAssetBalance returns the balance of the given asset.
+	GetAssetBalance(ctx context.Context, assetId []byte) (
+		uint64, error)
+
+	// DeriveNewKeys derives a new internal and script key.
+	DeriveNewKeys(ctx context.Context) (asset.ScriptKey,
+		keychain.KeyDescriptor, error)
+
+	// AddHoldInvoice adds a new hold invoice.
+	AddHoldInvoice(ctx context.Context, pHash lntypes.Hash,
+		assetId []byte, assetAmt uint64, memo string) (
+		*tapchannelrpc.AddInvoiceResponse, error)
+
+	// ImportProofFile imports the proof file and returns the last proof.
+	ImportProofFile(ctx context.Context, rawProofFile []byte) (
+		*proof.Proof, error)
+
+	// SendPayment pays a payment request.
+	SendPayment(ctx context.Context,
+		invoice string, assetId []byte) (chan *tapchannelrpc.SendPaymentResponse,
+		chan error, error)
+}
+
+// SwapStore is an interface that groups the methods required to store swap
+// information.
+type SwapStore interface {
+	// CreateAssetSwapOut creates a new swap out in the store.
+	CreateAssetSwapOut(ctx context.Context, swap *SwapOut) error
+
+	// UpdateAssetSwapHtlcOutpoint updates the htlc outpoint of a swap out.
+	UpdateAssetSwapHtlcOutpoint(ctx context.Context, swapHash lntypes.Hash,
+		outpoint *wire.OutPoint, confirmationHeight int32) error
+
+	// UpdateAssetSwapOutProof updates the proof of a swap out.
+	UpdateAssetSwapOutProof(ctx context.Context, swapHash lntypes.Hash,
+		rawProof []byte) error
+
+	// UpdateAssetSwapOutSweepTx updates the sweep tx of a swap out.
+	UpdateAssetSwapOutSweepTx(ctx context.Context,
+		swapHash lntypes.Hash, sweepTxid chainhash.Hash,
+		confHeight int32, sweepPkscript []byte) error
+
+	// InsertAssetSwapUpdate inserts a new swap update in the store.
+	InsertAssetSwapUpdate(ctx context.Context,
+		swapHash lntypes.Hash, state fsm.StateType) error
+
+	UpdateAssetSwapOutPreimage(ctx context.Context,
+		swapHash lntypes.Hash, preimage lntypes.Preimage) error
+}
+
+// BlockHeightSubscriber is responsible for subscribing to the expiry height
+// of a swap, as well as getting the current block height.
+type BlockHeightSubscriber interface {
+	// SubscribeExpiry subscribes to the expiry of a swap. It returns true
+	// if the expiry is already past. Otherwise, it returns false and calls
+	// the expiryFunc when the expiry height is reached.
+	SubscribeExpiry(swapHash [32]byte,
+		expiryHeight int32, expiryFunc func()) bool
+	// GetBlockHeight returns the current block height.
+	GetBlockHeight() int32
+}
+
+// InvoiceSubscriber is responsible for subscribing to an invoice.
+type InvoiceSubscriber interface {
+	// SubscribeInvoice subscribes to an invoice. The update callback is
+	// called when the invoice is updated and the error callback is called
+	// when an error occurs.
+	SubscribeInvoice(ctx context.Context, invoiceHash lntypes.Hash,
+		updateCallback func(lndclient.InvoiceUpdate, error)) error
+}
+
+// TxConfirmationSubscriber is responsible for subscribing to the confirmation
+// of a transaction.
+type TxConfirmationSubscriber interface {
+
+	// SubscribeTxConfirmation subscribes to the confirmation of a
+	// pkscript on the chain. The callback is called when the pkscript is
+	// confirmed or when an error occurs.
+	SubscribeTxConfirmation(ctx context.Context, swapHash lntypes.Hash,
+		txid *chainhash.Hash, pkscript []byte, numConfs int32,
+		eightHint int32, cb func(*chainntnfs.TxConfirmation, error)) error
+}
+
+// ExchangeRateProvider is responsible for providing the exchange rate between
+// assets.
+type ExchangeRateProvider interface {
+	// GetSatsPerAssetUnit returns the amount of satoshis per asset unit.
+	GetSatsPerAssetUnit(assetId []byte) (btcutil.Amount, error)
+}
diff --git a/assets/log.go b/assets/log.go
new file mode 100644
index 000000000..70981c586
--- /dev/null
+++ b/assets/log.go
@@ -0,0 +1,26 @@
+package assets
+
+import (
+	"github.com/btcsuite/btclog/v2"
+	"github.com/lightningnetwork/lnd/build"
+)
+
+// Subsystem defines the sub system name of this package.
+const Subsystem = "ASSETS"
+
+// log is a logger that is initialized with no output filters.  This
+// means the package will not perform any logging by default until the caller
+// requests it.
+var log btclog.Logger
+
+// The default amount of logging is none.
+func init() {
+	UseLogger(build.NewSubLogger(Subsystem, nil))
+}
+
+// UseLogger uses a specified Logger to output package logging info.
+// This should be used in preference to SetLogWriter if the caller is also
+// using btclog.
+func UseLogger(logger btclog.Logger) {
+	log = logger
+}
diff --git a/assets/manager.go b/assets/manager.go
new file mode 100644
index 000000000..6ca6dba29
--- /dev/null
+++ b/assets/manager.go
@@ -0,0 +1,206 @@
+package assets
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	"github.com/lightninglabs/lndclient"
+	"github.com/lightninglabs/loop/fsm"
+	loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
+	"github.com/lightninglabs/loop/utils"
+	"github.com/lightninglabs/taproot-assets/taprpc"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+const (
+	// ClientKeyFamily is the key family for the assets swap client.
+	ClientKeyFamily = 696969
+)
+
+// Config holds the configuration for the assets swap manager.
+type Config struct {
+	AssetClient *TapdClient
+	Wallet      lndclient.WalletKitClient
+	// ExchangeRateProvider is the exchange rate provider.
+	ExchangeRateProvider *FixedExchangeRateProvider
+	Signer               lndclient.SignerClient
+	ChainNotifier        lndclient.ChainNotifierClient
+	Router               lndclient.RouterClient
+	LndClient            lndclient.LightningClient
+	Store                *PostgresStore
+	ServerClient         loop_rpc.AssetsSwapServerClient
+}
+
+// AssetsSwapManager handles the lifecycle of asset swaps.
+type AssetsSwapManager struct {
+	cfg *Config
+
+	expiryManager *utils.ExpiryManager
+	txConfManager *utils.TxSubscribeConfirmationManager
+
+	blockHeight    int32
+	runCtx         context.Context
+	activeSwapOuts map[lntypes.Hash]*OutFSM
+
+	sync.Mutex
+}
+
+// NewAssetSwapServer creates a new assets swap manager.
+func NewAssetSwapServer(config *Config) *AssetsSwapManager {
+	return &AssetsSwapManager{
+		cfg: config,
+
+		activeSwapOuts: make(map[lntypes.Hash]*OutFSM),
+	}
+}
+
+// Run is the main loop for the assets swap manager.
+func (m *AssetsSwapManager) Run(ctx context.Context, blockHeight int32) error {
+	m.runCtx = ctx
+	m.blockHeight = blockHeight
+
+	// Get our tapd client info.
+	tapdInfo, err := m.cfg.AssetClient.GetInfo(
+		ctx, &taprpc.GetInfoRequest{},
+	)
+	if err != nil {
+		return err
+	}
+	log.Infof("Tapd info: %v", tapdInfo)
+
+	// Create our subscriptionManagers.
+	m.expiryManager = utils.NewExpiryManager(m.cfg.ChainNotifier)
+	m.txConfManager = utils.NewTxSubscribeConfirmationManager(
+		m.cfg.ChainNotifier,
+	)
+
+	// Start the expiry manager.
+	errChan := make(chan error, 1)
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		err := m.expiryManager.Start(ctx, blockHeight)
+		if err != nil {
+			log.Errorf("Expiry manager failed: %v", err)
+			errChan <- err
+		}
+	}()
+
+	// Recover all the active asset swap outs from the database.
+	err = m.recoverSwapOuts(ctx)
+	if err != nil {
+		return err
+	}
+
+	for {
+		select {
+		case err := <-errChan:
+			return err
+
+		case <-ctx.Done():
+			wg.Wait()
+			return nil
+		}
+	}
+}
+
+// NewSwapOut creates a new asset swap out using the amount and asset id.
+// It will wait for the fsm to be in the payprepay state before returning.
+func (m *AssetsSwapManager) NewSwapOut(ctx context.Context,
+	amt uint64, asset []byte) (*OutFSM, error) {
+
+	// Create a new out fsm.
+	outFSM := NewOutFSM(m.getFSMOutConfig())
+
+	// Send the initial event to the fsm.
+	err := outFSM.SendEvent(
+		m.runCtx, OnRequestAssetOut, &InitSwapOutContext{
+			Amount:          amt,
+			AssetId:         asset,
+			BlockHeightHint: uint32(m.blockHeight),
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+	// Check if the fsm has an error.
+	if outFSM.LastActionError != nil {
+		return nil, outFSM.LastActionError
+	}
+
+	// Wait for the fsm to be in the state we expect.
+	err = outFSM.DefaultObserver.WaitForState(
+		ctx, time.Second*15, PayPrepay,
+		fsm.WithAbortEarlyOnErrorOption(),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// Add the swap to the active swap outs.
+	m.Lock()
+	m.activeSwapOuts[outFSM.SwapOut.SwapHash] = outFSM
+	m.Unlock()
+
+	return outFSM, nil
+}
+
+// recoverSwapOuts recovers all the active asset swap outs from the database.
+func (m *AssetsSwapManager) recoverSwapOuts(ctx context.Context) error {
+	// Fetch all the active asset swap outs from the database.
+	activeSwapOuts, err := m.cfg.Store.GetActiveAssetOuts(ctx)
+	if err != nil {
+		return err
+	}
+
+	for _, swapOut := range activeSwapOuts {
+		log.Debugf("Recovering asset out %v with state %v",
+			swapOut.SwapHash, swapOut.State)
+
+		swapOutFSM := NewOutFSMFromSwap(
+			m.getFSMOutConfig(), swapOut,
+		)
+
+		m.Lock()
+		m.activeSwapOuts[swapOut.SwapHash] = swapOutFSM
+		m.Unlock()
+
+		// As SendEvent can block, we'll start a goroutine to process
+		// the event.
+		go func() {
+			err := swapOutFSM.SendEvent(ctx, OnRecover, nil)
+			if err != nil {
+				log.Errorf("FSM %v Error sending recover "+
+					"event %v, state: %v",
+					swapOutFSM.SwapOut.SwapHash,
+					err, swapOutFSM.SwapOut.State)
+			}
+		}()
+	}
+
+	return nil
+}
+
+// getFSMOutConfig returns a fsmconfig from the manager.
+func (m *AssetsSwapManager) getFSMOutConfig() *FSMConfig {
+	return &FSMConfig{
+		TapdClient:            m.cfg.AssetClient,
+		AssetClient:           m.cfg.ServerClient,
+		BlockHeightSubscriber: m.expiryManager,
+		TxConfSubscriber:      m.txConfManager,
+		ExchangeRateProvider:  m.cfg.ExchangeRateProvider,
+		Wallet:                m.cfg.Wallet,
+
+		Store:  m.cfg.Store,
+		Signer: m.cfg.Signer,
+	}
+}
+
+// ListSwapOuts lists all the asset swap outs in the database.
+func (m *AssetsSwapManager) ListSwapOuts(ctx context.Context) ([]*SwapOut,
+	error) {
+
+	return m.cfg.Store.GetAllAssetOuts(ctx)
+}
diff --git a/assets/out_fsm.go b/assets/out_fsm.go
new file mode 100644
index 000000000..08b6a4604
--- /dev/null
+++ b/assets/out_fsm.go
@@ -0,0 +1,608 @@
+package assets
+
+import (
+	"bytes"
+	"context"
+	"errors"
+
+	"github.com/btcsuite/btcd/btcutil/psbt"
+	"github.com/btcsuite/btcd/txscript"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/lndclient"
+	"github.com/lightninglabs/loop/fsm"
+	"github.com/lightninglabs/loop/swapserverrpc"
+	"github.com/lightninglabs/taproot-assets/address"
+	"github.com/lightninglabs/taproot-assets/commitment"
+	"github.com/lightninglabs/taproot-assets/proof"
+	"github.com/lightninglabs/taproot-assets/tapscript"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
+)
+
+const (
+	// Limit the observers transition observation stack to 15 entries.
+	defaultObserverSize = 15
+)
+
+// States.
+const (
+	// Init is the initial state of the swap.
+	Init fsm.StateType = "Init"
+
+	// PayPrepay is the state where we are waiting for the
+	// prepay invoice to be accepted.
+	PayPrepay fsm.StateType = "PayPrepay"
+
+	// FetchProof is the state where the prepay invoice has been
+	// accepted.
+	FetchProof fsm.StateType = "FetchProof"
+
+	// WaitForBlock is the state where we are waiting for the next block
+	// to be mined.
+	WaitForBlock fsm.StateType = "WaitForBlock"
+
+	// WaitForHtlcConfirmed is the state where the htlc transaction
+	// has been broadcast.
+	WaitForHtlcConfirmed fsm.StateType = "WaitForHtlcConfirmed"
+
+	// HtlcTxConfirmed is the state where the htlc transaction
+	// has been confirmed.
+	HtlcTxConfirmed fsm.StateType = "HtlcTxConfirmed"
+
+	// SweepHtlc is the state where we are creating the swap
+	// invoice.
+	SweepHtlc fsm.StateType = "SweepHtlc"
+
+	// WaitForSweepConfirmed is the state where we are waiting for the swap
+	// payment to be made. This is after we have given the receiver the
+	// taproot assets proof.
+	WaitForSweepConfirmed fsm.StateType = "WaitForSweepConfirmed"
+
+	// Finished is the state where the swap has finished.
+	Finished fsm.StateType = "Finished"
+
+	// FinishedTimeout is the state where the swap has finished due to
+	// a timeout.
+	FinishedTimeout fsm.StateType = "FinishedTimeout"
+
+	// Failed is the state where the swap has failed.
+	Failed fsm.StateType = "Failed"
+)
+
+var (
+	finishedStates = []fsm.StateType{
+		Finished, FinishedTimeout, Failed,
+	}
+)
+
+// Events.
+var (
+	// OnRequestAssetOut is the event where the server receives a swap
+	// request from the client.
+	OnRequestAssetOut = fsm.EventType("OnRequestAssetOut")
+
+	// onAssetOutInit is the event where the server has initialized the
+	// swap.
+	onAssetOutInit = fsm.EventType("OnAssetOutInit")
+
+	// onPrepaySettled is the event where the prepay invoice has been
+	// accepted.
+	onPrepaySettled = fsm.EventType("onPrepaySettled")
+
+	onWaitForBlock = fsm.EventType("onWaitForBlock")
+
+	onBlockReceived = fsm.EventType("onBlockReceived")
+
+	onProofReceived = fsm.EventType("OnProofReceived")
+
+	onHtlcTxConfirmed = fsm.EventType("onHtlcTxConfirmed")
+
+	onSwapPreimageReceived = fsm.EventType("OnSwapPreimageReceived")
+
+	// onHtlcSuccessSweep is the event where the htlc has timed out and we
+	// are trying to sweep the htlc output.
+	onHtlcSuccessSweep = fsm.EventType("onHtlcSuccessSweep")
+
+	// onSweepTxConfirmed is the event where the sweep transaction has been
+	// confirmed.
+	onSweepTxConfirmed = fsm.EventType("OnSweepTxConfirmed")
+
+	// OnRecover is the event where the swap is being recovered.
+	OnRecover = fsm.EventType("OnRecover")
+)
+
+// FSMConfig contains the configuration for the FSM.
+type FSMConfig struct {
+	// TapdClient is the client to interact with the taproot asset daemon.
+	TapdClient AssetClient
+
+	// AssetClient is the client to interact with the asset swap server.
+	AssetClient swapserverrpc.AssetsSwapServerClient
+
+	// BlockHeightSubscriber is the subscriber to the block height.
+	BlockHeightSubscriber BlockHeightSubscriber
+
+	// TxConfSubscriber is the subscriber to the transaction confirmation.
+	TxConfSubscriber TxConfirmationSubscriber
+
+	// ExchangeRateProvider is the provider for the exchange rate.
+	ExchangeRateProvider ExchangeRateProvider
+
+	// Wallet is the wallet client.
+	Wallet lndclient.WalletKitClient
+
+	// Signer is the signer client.
+	Signer lndclient.SignerClient
+
+	// Store is the swap store.
+	Store SwapStore
+
+	// AddrParams are the chain parameters for addresses.
+	AddrParams *address.ChainParams
+}
+
+type OutFSM struct {
+	*fsm.StateMachine
+
+	cfg *FSMConfig
+
+	// SwapOut contains all the information about the swap.
+	SwapOut *SwapOut
+
+	// PrepayInvoice is the prepay invoice that we are paying to initiate
+	// the swap.
+	PrepayInvoice string
+
+	// SwapInvoice is the swap invoice that we are sending to the receiver.
+	SwapInvoice string
+
+	// HtlcProof is the htlc proof that we use to sweep the htlc output.
+	HtlcProof *proof.Proof
+}
+
+// NewOutFSM creates a new OutFSM.
+func NewOutFSM(cfg *FSMConfig) *OutFSM {
+	out := &SwapOut{
+		State: fsm.EmptyState,
+	}
+
+	return NewOutFSMFromSwap(cfg, out)
+}
+
+// NewOutFSMFromSwap creates a new OutFSM from a existing swap.
+func NewOutFSMFromSwap(cfg *FSMConfig, swap *SwapOut,
+) *OutFSM {
+
+	outFSM := &OutFSM{
+		cfg:     cfg,
+		SwapOut: swap,
+	}
+
+	outFSM.StateMachine = fsm.NewStateMachineWithState(
+		outFSM.GetStates(), outFSM.SwapOut.State, defaultObserverSize,
+	)
+	outFSM.ActionEntryFunc = outFSM.updateSwap
+
+	return outFSM
+}
+
+// GetStates returns the swap out state machine.
+func (o *OutFSM) GetStates() fsm.States {
+	return fsm.States{
+		fsm.EmptyState: fsm.State{
+			Transitions: fsm.Transitions{
+				OnRequestAssetOut: Init,
+			},
+			Action: nil,
+		},
+		Init: fsm.State{
+			Transitions: fsm.Transitions{
+				onAssetOutInit: PayPrepay,
+				// Before the htlc has been signed we can always
+				// fail the swap.
+				OnRecover:   Failed,
+				fsm.OnError: Failed,
+			},
+			Action: o.InitSwapOut,
+		},
+		PayPrepay: fsm.State{
+			Transitions: fsm.Transitions{
+				onPrepaySettled: FetchProof,
+				fsm.OnError:     Failed,
+				OnRecover:       Failed,
+			},
+			Action: o.PayPrepay,
+		},
+		FetchProof: fsm.State{
+			Transitions: fsm.Transitions{
+				onWaitForBlock:  WaitForBlock,
+				onProofReceived: WaitForHtlcConfirmed,
+				fsm.OnError:     Failed,
+				OnRecover:       FetchProof,
+			},
+			Action: o.FetchProof,
+		},
+		WaitForBlock: fsm.State{
+			Transitions: fsm.Transitions{
+				onBlockReceived: FetchProof,
+				OnRecover:       FetchProof,
+			},
+			Action: o.waitForBlock,
+		},
+		WaitForHtlcConfirmed: fsm.State{
+			Transitions: fsm.Transitions{
+				onHtlcTxConfirmed: HtlcTxConfirmed,
+				fsm.OnError:       Failed,
+				OnRecover:         WaitForHtlcConfirmed,
+			},
+			Action: o.subscribeToHtlcTxConfirmed,
+		},
+		HtlcTxConfirmed: fsm.State{
+			Transitions: fsm.Transitions{
+				onSwapPreimageReceived: SweepHtlc,
+				// Todo(sputn1ck) change to wait for expiry state.
+				fsm.OnError: Failed,
+				OnRecover:   HtlcTxConfirmed,
+			},
+			Action: o.sendSwapPayment,
+		},
+		SweepHtlc: fsm.State{
+			Transitions: fsm.Transitions{
+				onHtlcSuccessSweep: WaitForSweepConfirmed,
+				fsm.OnError:        SweepHtlc,
+				OnRecover:          SweepHtlc,
+			},
+			Action: o.publishSweepTx,
+		},
+		WaitForSweepConfirmed: fsm.State{
+			Transitions: fsm.Transitions{
+				onSweepTxConfirmed: Finished,
+				fsm.OnError:        WaitForSweepConfirmed,
+				OnRecover:          WaitForSweepConfirmed,
+			},
+			Action: o.subscribeSweepConf,
+		},
+		Finished: fsm.State{
+			Action: fsm.NoOpAction,
+		},
+		Failed: fsm.State{
+			Action: fsm.NoOpAction,
+		},
+	}
+}
+
+// // getSwapCopy returns a copy of the swap that is safe to be used from the
+// // caller.
+//  func (o *OutFSM) getSwapCopy() *SwapOut {
+
+// updateSwap is called after every action and updates the swap in the db.
+func (o *OutFSM) updateSwap(ctx context.Context,
+	notification fsm.Notification) {
+
+	// Skip the update if the swap is not yet initialized.
+	if o.SwapOut == nil {
+		return
+	}
+
+	o.Infof("Current: %v", notification.NextState)
+
+	o.SwapOut.State = notification.NextState
+
+	// If we're in the early stages we don't have created the swap in the
+	// store yet and won't need to update it.
+	if o.SwapOut.State == Init || (notification.PreviousState == Init &&
+		notification.NextState == Failed) {
+
+		return
+	}
+
+	err := o.cfg.Store.InsertAssetSwapUpdate(
+		ctx, o.SwapOut.SwapHash, o.SwapOut.State,
+	)
+	if err != nil {
+		log.Errorf("Error updating swap : %v", err)
+		return
+	}
+}
+
+// getHtlcPkscript returns the pkscript of the htlc output.
+func (o *OutFSM) getHtlcPkscript() ([]byte, error) {
+	// First fetch the proof.
+	proof, err := o.getHtlcProof()
+	if err != nil {
+		return nil, err
+	}
+
+	// // Verify that the asset script matches the one predicted.
+	// assetScriptkey, _, _, _, err := createOpTrueLeaf()
+	// if err != nil {
+	// 	return nil, err
+	// }
+
+	// o.Debugf("Asset script key: %x", assetScriptkey.PubKey.SerializeCompressed())
+	// o.Debugf("Proof script key: %x", proof.Asset.ScriptKey.PubKey.SerializeCompressed())
+	// if !bytes.Equal(
+	// 	proof.Asset.ScriptKey.PubKey.SerializeCompressed(),
+	// 	assetScriptkey.PubKey.SerializeCompressed(),
+	// ) {
+	// 	return nil, fmt.Errorf("asset script key mismatch")
+	// }
+
+	assetCpy := proof.Asset.Copy()
+	assetCpy.PrevWitnesses[0].SplitCommitment = nil
+	sendCommitment, err := commitment.NewAssetCommitment(
+		assetCpy,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	version := commitment.TapCommitmentV2
+	assetCommitment, err := commitment.NewTapCommitment(
+		&version, sendCommitment,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	siblingPreimage, err := o.SwapOut.GetSiblingPreimage()
+	if err != nil {
+		return nil, err
+	}
+
+	siblingHash, err := siblingPreimage.TapHash()
+	if err != nil {
+		return nil, err
+	}
+
+	btcInternalKey, err := o.SwapOut.GetAggregateKey()
+	if err != nil {
+		return nil, err
+	}
+
+	anchorPkScript, err := tapscript.PayToAddrScript(
+		*btcInternalKey, siblingHash, *assetCommitment,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return anchorPkScript, nil
+}
+
+// publishPreimageSweep publishes and logs the preimage sweep transaction.
+func (o *OutFSM) publishPreimageSweep(ctx context.Context,
+	addr *address.Tap) (*wire.OutPoint, []byte, error) {
+
+	// Check if we have the proof in memory.
+	htlcProof, err := o.getHtlcProof()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	sweepVpkt, err := CreateOpTrueSweepVpkt(
+		ctx, []*proof.Proof{htlcProof}, addr, o.cfg.AddrParams,
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	feeRate, err := o.cfg.Wallet.EstimateFeeRate(
+		ctx, defaultHtlcFeeConfTarget,
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// We'll now commit the vpkt in the btcpacket.
+	sweepBtcPacket, activeAssets, passiveAssets, commitResp, err :=
+		o.cfg.TapdClient.PrepareAndCommitVirtualPsbts(
+			// todo check change desc and params.
+			ctx, sweepVpkt, feeRate.FeePerVByte(), nil, nil,
+		)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	witness, err := o.createPreimageWitness(
+		ctx, sweepBtcPacket, htlcProof,
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var buf bytes.Buffer
+	err = psbt.WriteTxWitness(&buf, witness)
+	if err != nil {
+		return nil, nil, err
+	}
+	sweepBtcPacket.Inputs[0].SighashType = txscript.SigHashDefault
+	sweepBtcPacket.Inputs[0].FinalScriptWitness = buf.Bytes()
+
+	signedBtcPacket, err := o.cfg.Wallet.SignPsbt(ctx, sweepBtcPacket)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	finalizedBtcPacket, _, err := o.cfg.Wallet.FinalizePsbt(
+		ctx, signedBtcPacket, "",
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pkScript := finalizedBtcPacket.UnsignedTx.TxOut[0].PkScript
+
+	// Now we'll publish and log the transfer.
+	sendResp, err := o.cfg.TapdClient.LogAndPublish(
+		ctx, finalizedBtcPacket, activeAssets, passiveAssets,
+		commitResp,
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	sweepAnchor := sendResp.Transfer.Outputs[0].Anchor
+
+	outPoint, err := wire.NewOutPointFromString(sweepAnchor.Outpoint)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return outPoint, pkScript, nil
+}
+
+// getHtlcProof returns the htlc proof for the swap. If the proof is not
+// in memory, we will recreate it from the stored proof file.
+func (o *OutFSM) getHtlcProof() (*proof.Proof, error) {
+	// Check if we have the proof in memory.
+	if o.HtlcProof != nil {
+		return o.HtlcProof, nil
+	}
+
+	// Parse the proof.
+	htlcProofFile, err := proof.DecodeFile(o.SwapOut.RawHtlcProof)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get the proofs.
+	htlcProof, err := htlcProofFile.LastProof()
+	if err != nil {
+		return nil, err
+	}
+
+	return htlcProof, nil
+}
+
+// createPreimageWitness creates a preimage witness for the swap.
+func (o *OutFSM) createPreimageWitness(ctx context.Context,
+	sweepBtcPacket *psbt.Packet, htlcProof *proof.Proof) (wire.TxWitness,
+	error) {
+
+	assetTxOut := &wire.TxOut{
+		PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript,
+		Value:    sweepBtcPacket.Inputs[0].WitnessUtxo.Value,
+	}
+	feeTxOut := &wire.TxOut{
+		PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript,
+		Value:    sweepBtcPacket.Inputs[1].WitnessUtxo.Value,
+	}
+
+	sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1
+
+	successScript, err := o.SwapOut.GetSuccessScript()
+	if err != nil {
+		return nil, err
+	}
+
+	signDesc := &lndclient.SignDescriptor{
+		KeyDesc: keychain.KeyDescriptor{
+			KeyLocator: o.SwapOut.ClientKeyLocator,
+		},
+		SignMethod:    input.TaprootScriptSpendSignMethod,
+		WitnessScript: successScript,
+		Output:        assetTxOut,
+		InputIndex:    0,
+	}
+	sig, err := o.cfg.Signer.SignOutputRaw(
+		ctx, sweepBtcPacket.UnsignedTx, []*lndclient.SignDescriptor{signDesc},
+		[]*wire.TxOut{assetTxOut, feeTxOut},
+	)
+	if err != nil {
+		return nil, err
+	}
+	taprootAssetRoot, err := o.SwapOut.genTaprootAssetRootFromProof(
+		htlcProof,
+	)
+	if err != nil {
+		return nil, err
+	}
+	successControlBlock, err := o.SwapOut.GenSuccessBtcControlBlock(
+		taprootAssetRoot,
+	)
+	if err != nil {
+		return nil, err
+	}
+	controlBlockBytes, err := successControlBlock.ToBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	return wire.TxWitness{
+		o.SwapOut.SwapPreimage[:],
+		sig[0],
+		successScript,
+		controlBlockBytes,
+	}, nil
+}
+
+// Infof logs an info message with the swap hash as prefix.
+func (o *OutFSM) Infof(format string, args ...interface{}) {
+	log.Infof(
+		"Swap %v: "+format,
+		append(
+			[]interface{}{o.SwapOut.SwapHash},
+			args...,
+		)...,
+	)
+}
+
+// Debugf logs a debug message with the swap hash as prefix.
+func (o *OutFSM) Debugf(format string, args ...interface{}) {
+	log.Debugf(
+		"Swap %v: "+format,
+		append(
+			[]interface{}{o.SwapOut.SwapHash},
+			args...,
+		)...,
+	)
+}
+
+// Errorf logs an error message with the swap hash as prefix.
+func (o *OutFSM) Errorf(format string, args ...interface{}) {
+	log.Errorf(
+		"Swap %v: "+format,
+		append(
+			[]interface{}{o.SwapOut.SwapHash},
+			args...,
+		)...,
+	)
+}
+func (o *OutFSM) findPkScript(tx *wire.MsgTx) (*wire.OutPoint,
+	error) {
+
+	pkScript, err := o.getHtlcPkscript()
+	if err != nil {
+		return nil, err
+	}
+
+	for i, out := range tx.TxOut {
+		if bytes.Equal(out.PkScript, pkScript) {
+			txHash := tx.TxHash()
+			return wire.NewOutPoint(&txHash, uint32(i)), nil
+		}
+	}
+	return nil, errors.New("pkscript not found")
+}
+
+// IsFinishedState returns true if the passed state is a finished state.
+func IsFinishedState(state fsm.StateType) bool {
+	for _, s := range finishedStates {
+		if s == state {
+			return true
+		}
+	}
+
+	return false
+}
+
+// FinishedStates returns a string slice of all finished states.
+func FinishedStates() []string {
+	states := make([]string, 0, len(finishedStates))
+	for _, s := range finishedStates {
+		states = append(states, string(s))
+	}
+
+	return states
+}
diff --git a/assets/rateprovider.go b/assets/rateprovider.go
new file mode 100644
index 000000000..58a48b943
--- /dev/null
+++ b/assets/rateprovider.go
@@ -0,0 +1,23 @@
+package assets
+
+import "github.com/btcsuite/btcd/btcutil"
+
+const (
+	fixedPrice = 100
+)
+
+// FixedExchangeRateProvider is a fixed exchange rate provider.
+type FixedExchangeRateProvider struct {
+}
+
+// NewFixedExchangeRateProvider creates a new fixed exchange rate provider.
+func NewFixedExchangeRateProvider() *FixedExchangeRateProvider {
+	return &FixedExchangeRateProvider{}
+}
+
+// GetSatsPerAssetUnit returns the fixed price in sats per asset unit.
+func (e *FixedExchangeRateProvider) GetSatsPerAssetUnit(assetId []byte) (
+	btcutil.Amount, error) {
+
+	return btcutil.Amount(fixedPrice), nil
+}
diff --git a/assets/server.go b/assets/server.go
new file mode 100644
index 000000000..d1c0691ab
--- /dev/null
+++ b/assets/server.go
@@ -0,0 +1,126 @@
+package assets
+
+import (
+	"context"
+
+	clientrpc "github.com/lightninglabs/loop/looprpc"
+	"github.com/lightninglabs/loop/swapserverrpc"
+	"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
+)
+
+type AssetsClientServer struct {
+	manager *AssetsSwapManager
+
+	clientrpc.UnimplementedAssetsClientServer
+}
+
+func NewAssetsServer(manager *AssetsSwapManager) *AssetsClientServer {
+	return &AssetsClientServer{
+		manager: manager,
+	}
+}
+
+func (a *AssetsClientServer) SwapOut(ctx context.Context,
+	req *clientrpc.SwapOutRequest) (*clientrpc.SwapOutResponse, error) {
+
+	swap, err := a.manager.NewSwapOut(
+		ctx, req.Amt, req.Asset,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return &clientrpc.SwapOutResponse{
+		SwapStatus: &clientrpc.AssetSwapStatus{
+			SwapHash:   swap.SwapOut.SwapHash[:],
+			SwapStatus: string(swap.SwapOut.State),
+		},
+	}, nil
+}
+
+func (a *AssetsClientServer) ListAssetSwaps(ctx context.Context,
+	_ *clientrpc.ListAssetSwapsRequest) (*clientrpc.ListAssetSwapsResponse,
+	error) {
+
+	swaps, err := a.manager.ListSwapOuts(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	rpcSwaps := make([]*clientrpc.AssetSwapStatus, 0, len(swaps))
+	for _, swap := range swaps {
+		rpcSwaps = append(rpcSwaps, &clientrpc.AssetSwapStatus{
+			SwapHash:   swap.SwapHash[:],
+			SwapStatus: string(swap.State),
+		})
+	}
+
+	return &clientrpc.ListAssetSwapsResponse{
+		SwapStatus: rpcSwaps,
+	}, nil
+}
+
+func (a *AssetsClientServer) ClientListAvailableAssets(ctx context.Context,
+	req *clientrpc.ClientListAvailableAssetsRequest,
+) (*clientrpc.ClientListAvailableAssetsResponse, error) {
+
+	assets, err := a.manager.cfg.ServerClient.ListAvailableAssets(
+		ctx, &swapserverrpc.ListAvailableAssetsRequest{},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	availableAssets := make([]*clientrpc.Asset, 0, len(assets.Assets))
+
+	for _, asset := range assets.Assets {
+		clientAsset := &clientrpc.Asset{
+			AssetId:     asset.AssetId,
+			SatsPerUnit: asset.CurrentSatsPerAssetUnit,
+			Name:        "Asset unknown in known universes",
+		}
+		universeRes, err := a.manager.cfg.AssetClient.QueryAssetRoots(
+			ctx, &universerpc.AssetRootQuery{
+				Id: &universerpc.ID{
+					Id: &universerpc.ID_AssetId{
+						AssetId: asset.AssetId,
+					},
+					ProofType: universerpc.ProofType_PROOF_TYPE_ISSUANCE,
+				},
+			},
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		if universeRes.IssuanceRoot != nil {
+			clientAsset.Name = universeRes.IssuanceRoot.AssetName
+		}
+
+		availableAssets = append(availableAssets, clientAsset)
+	}
+
+	return &clientrpc.ClientListAvailableAssetsResponse{
+		AvailableAssets: availableAssets,
+	}, nil
+}
+func (a *AssetsClientServer) ClientGetAssetSwapOutQuote(ctx context.Context,
+	req *clientrpc.ClientGetAssetSwapOutQuoteRequest,
+) (*clientrpc.ClientGetAssetSwapOutQuoteResponse, error) {
+
+	// Get the quote from the server.
+	quoteRes, err := a.manager.cfg.ServerClient.QuoteAssetLoopOut(
+		ctx, &swapserverrpc.QuoteAssetLoopOutRequest{
+			Amount: req.Amt,
+			Asset:  req.Asset,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return &clientrpc.ClientGetAssetSwapOutQuoteResponse{
+		SwapFee:     quoteRes.SwapFeeRate,
+		PrepayAmt:   quoteRes.FixedPrepayAmt,
+		SatsPerUnit: quoteRes.CurrentSatsPerAssetUnit,
+	}, nil
+}
diff --git a/assets/store.go b/assets/store.go
new file mode 100644
index 000000000..27f419b43
--- /dev/null
+++ b/assets/store.go
@@ -0,0 +1,289 @@
+package assets
+
+import (
+	"context"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/loop/assets/htlc"
+	"github.com/lightninglabs/loop/fsm"
+	"github.com/lightninglabs/loop/loopdb"
+	"github.com/lightninglabs/loop/loopdb/sqlc"
+	"github.com/lightningnetwork/lnd/clock"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+// BaseDB is the interface that contains all the queries generated
+// by sqlc for the instantout table.
+type BaseDB interface {
+	// ExecTx allows for executing a function in the context of a database
+	// transaction.
+	ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
+		txBody func(*sqlc.Queries) error) error
+
+	CreateAssetSwap(ctx context.Context, arg sqlc.CreateAssetSwapParams) error
+	CreateAssetOutSwap(ctx context.Context, swapHash []byte) error
+	GetAllAssetOutSwaps(ctx context.Context) ([]sqlc.GetAllAssetOutSwapsRow, error)
+	GetAssetOutSwap(ctx context.Context, swapHash []byte) (sqlc.GetAssetOutSwapRow, error)
+	InsertAssetSwapUpdate(ctx context.Context, arg sqlc.InsertAssetSwapUpdateParams) error
+	UpdateAssetSwapHtlcTx(ctx context.Context, arg sqlc.UpdateAssetSwapHtlcTxParams) error
+	UpdateAssetSwapOutPreimage(ctx context.Context, arg sqlc.UpdateAssetSwapOutPreimageParams) error
+	UpdateAssetSwapOutProof(ctx context.Context, arg sqlc.UpdateAssetSwapOutProofParams) error
+	UpdateAssetSwapSweepTx(ctx context.Context, arg sqlc.UpdateAssetSwapSweepTxParams) error
+}
+
+// PostgresStore is the backing store for the instant out manager.
+type PostgresStore struct {
+	queries BaseDB
+	clock   clock.Clock
+}
+
+// NewPostgresStore creates a new PostgresStore.
+func NewPostgresStore(queries BaseDB) *PostgresStore {
+	return &PostgresStore{
+		queries: queries,
+		clock:   clock.NewDefaultClock(),
+	}
+}
+
+// CreateAssetSwapOut creates a new asset swap out in the database.
+func (p *PostgresStore) CreateAssetSwapOut(ctx context.Context,
+	swap *SwapOut) error {
+
+	params := sqlc.CreateAssetSwapParams{
+		SwapHash:         swap.SwapHash[:],
+		AssetID:          swap.AssetID,
+		Amt:              int64(swap.Amount),
+		SenderPubkey:     swap.SenderPubKey.SerializeCompressed(),
+		ReceiverPubkey:   swap.ReceiverPubKey.SerializeCompressed(),
+		CsvExpiry:        int32(swap.CsvExpiry),
+		InitiationHeight: int32(swap.InitiationHeight),
+		CreatedTime:      p.clock.Now(),
+		ServerKeyFamily:  int64(swap.ClientKeyLocator.Family),
+		ServerKeyIndex:   int64(swap.ClientKeyLocator.Index),
+	}
+
+	return p.queries.ExecTx(
+		ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
+			err := q.CreateAssetSwap(ctx, params)
+			if err != nil {
+				return err
+			}
+
+			return q.CreateAssetOutSwap(ctx, swap.SwapHash[:])
+		},
+	)
+}
+
+// UpdateAssetSwapHtlcOutpoint updates the htlc outpoint of the swap out in the
+// database.
+func (p *PostgresStore) UpdateAssetSwapHtlcOutpoint(ctx context.Context,
+	swapHash lntypes.Hash, outpoint *wire.OutPoint, confirmationHeight int32) error {
+
+	return p.queries.ExecTx(
+		ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
+			return q.UpdateAssetSwapHtlcTx(
+				ctx, sqlc.UpdateAssetSwapHtlcTxParams{
+					SwapHash:               swapHash[:],
+					HtlcTxid:               outpoint.Hash[:],
+					HtlcVout:               int32(outpoint.Index),
+					HtlcConfirmationHeight: confirmationHeight,
+				})
+		},
+	)
+}
+
+// UpdateAssetSwapOutProof updates the raw proof of the swap out in the
+// database.
+func (p *PostgresStore) UpdateAssetSwapOutProof(ctx context.Context,
+	swapHash lntypes.Hash, rawProof []byte) error {
+
+	return p.queries.ExecTx(
+		ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
+			return q.UpdateAssetSwapOutProof(
+				ctx, sqlc.UpdateAssetSwapOutProofParams{
+					SwapHash:     swapHash[:],
+					RawProofFile: rawProof,
+				})
+		},
+	)
+}
+
+// UpdateAssetSwapOutPreimage updates the preimage of the swap out in the
+// database.
+func (p *PostgresStore) UpdateAssetSwapOutPreimage(ctx context.Context,
+	swapHash lntypes.Hash, preimage lntypes.Preimage) error {
+
+	return p.queries.ExecTx(
+		ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
+			return q.UpdateAssetSwapOutPreimage(
+				ctx, sqlc.UpdateAssetSwapOutPreimageParams{
+					SwapHash:     swapHash[:],
+					SwapPreimage: preimage[:],
+				})
+		},
+	)
+}
+
+// UpdateAssetSwapOutSweepTx updates the sweep tx of the swap out in the
+// database.
+func (p *PostgresStore) UpdateAssetSwapOutSweepTx(ctx context.Context,
+	swapHash lntypes.Hash, sweepTxid chainhash.Hash, confHeight int32,
+	sweepPkscript []byte) error {
+
+	return p.queries.ExecTx(
+		ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
+			return q.UpdateAssetSwapSweepTx(
+				ctx, sqlc.UpdateAssetSwapSweepTxParams{
+					SwapHash:                swapHash[:],
+					SweepTxid:               sweepTxid[:],
+					SweepConfirmationHeight: confHeight,
+					SweepPkscript:           sweepPkscript,
+				})
+		},
+	)
+}
+
+// InsertAssetSwapUpdate inserts a new swap update in the database.
+func (p *PostgresStore) InsertAssetSwapUpdate(ctx context.Context,
+	swapHash lntypes.Hash, state fsm.StateType) error {
+
+	return p.queries.ExecTx(
+		ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
+			return q.InsertAssetSwapUpdate(
+				ctx, sqlc.InsertAssetSwapUpdateParams{
+					SwapHash:        swapHash[:],
+					UpdateState:     string(state),
+					UpdateTimestamp: p.clock.Now(),
+				})
+		},
+	)
+}
+
+// GetAllAssetOuts returns all the asset outs from the database.
+func (p *PostgresStore) GetAllAssetOuts(ctx context.Context) ([]*SwapOut, error) {
+	dbAssetOuts, err := p.queries.GetAllAssetOutSwaps(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	assetOuts := make([]*SwapOut, 0, len(dbAssetOuts))
+	for _, dbAssetOut := range dbAssetOuts {
+		assetOut, err := newSwapOutFromDB(
+			dbAssetOut.AssetSwap, dbAssetOut.AssetOutSwap,
+			dbAssetOut.UpdateState,
+		)
+		if err != nil {
+			return nil, err
+		}
+		assetOuts = append(assetOuts, assetOut)
+	}
+	return assetOuts, nil
+}
+
+// GetActiveAssetOuts returns all the active asset outs from the database.
+func (p *PostgresStore) GetActiveAssetOuts(ctx context.Context) ([]*SwapOut,
+	error) {
+
+	dbAssetOuts, err := p.queries.GetAllAssetOutSwaps(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	assetOuts := make([]*SwapOut, 0)
+	for _, dbAssetOut := range dbAssetOuts {
+		if IsFinishedState(fsm.StateType(dbAssetOut.UpdateState)) {
+			continue
+		}
+
+		assetOut, err := newSwapOutFromDB(
+			dbAssetOut.AssetSwap, dbAssetOut.AssetOutSwap,
+			dbAssetOut.UpdateState,
+		)
+		if err != nil {
+			return nil, err
+		}
+		assetOuts = append(assetOuts, assetOut)
+	}
+
+	return assetOuts, nil
+}
+
+// newSwapOutFromDB creates a new SwapOut from the databse rows.
+func newSwapOutFromDB(assetSwap sqlc.AssetSwap,
+	assetOutSwap sqlc.AssetOutSwap, state string) (
+	*SwapOut, error) {
+
+	swapHash, err := lntypes.MakeHash(assetSwap.SwapHash)
+	if err != nil {
+		return nil, err
+	}
+
+	var swapPreimage lntypes.Preimage
+	if assetSwap.SwapPreimage != nil {
+		swapPreimage, err = lntypes.MakePreimage(assetSwap.SwapPreimage)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	senderPubkey, err := btcec.ParsePubKey(assetSwap.SenderPubkey)
+	if err != nil {
+		return nil, err
+	}
+
+	receiverPubkey, err := btcec.ParsePubKey(assetSwap.ReceiverPubkey)
+	if err != nil {
+		return nil, err
+	}
+
+	var htlcOutpoint *wire.OutPoint
+	if assetSwap.HtlcTxid != nil {
+		htlcHash, err := chainhash.NewHash(assetSwap.HtlcTxid)
+		if err != nil {
+			return nil, err
+		}
+		htlcOutpoint = wire.NewOutPoint(
+			htlcHash, uint32(assetSwap.HtlcVout),
+		)
+	}
+
+	var sweepOutpoint *wire.OutPoint
+	if assetSwap.SweepTxid != nil {
+		sweepHash, err := chainhash.NewHash(assetSwap.SweepTxid)
+		if err != nil {
+			return nil, err
+		}
+		sweepOutpoint = wire.NewOutPoint(
+			sweepHash, 0,
+		)
+	}
+
+	return &SwapOut{
+		SwapKit: htlc.SwapKit{
+			SwapHash:       swapHash,
+			Amount:         uint64(assetSwap.Amt),
+			SenderPubKey:   senderPubkey,
+			ReceiverPubKey: receiverPubkey,
+			CsvExpiry:      uint32(assetSwap.CsvExpiry),
+			AssetID:        assetSwap.AssetID,
+		},
+		SwapPreimage:     swapPreimage,
+		State:            fsm.StateType(state),
+		InitiationHeight: uint32(assetSwap.InitiationHeight),
+		ClientKeyLocator: keychain.KeyLocator{
+			Family: keychain.KeyFamily(
+				assetSwap.ServerKeyFamily,
+			),
+			Index: uint32(assetSwap.ServerKeyIndex),
+		},
+		HtlcOutPoint:            htlcOutpoint,
+		HtlcConfirmationHeight:  uint32(assetSwap.HtlcConfirmationHeight),
+		SweepOutpoint:           sweepOutpoint,
+		SweepConfirmationHeight: uint32(assetSwap.SweepConfirmationHeight),
+		SweepPkscript:           assetSwap.SweepPkscript,
+		RawHtlcProof:            assetOutSwap.RawProofFile,
+	}, nil
+}
diff --git a/assets/store_test.go b/assets/store_test.go
new file mode 100644
index 000000000..306266dc3
--- /dev/null
+++ b/assets/store_test.go
@@ -0,0 +1,112 @@
+package assets
+
+import (
+	"context"
+	"crypto/rand"
+	"encoding/hex"
+	"testing"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/loop/assets/htlc"
+	"github.com/lightninglabs/loop/fsm"
+	"github.com/lightninglabs/loop/loopdb"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lntypes"
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	defaultClientPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d")
+	defaultClientPubkey, _      = btcec.ParsePubKey(defaultClientPubkeyBytes)
+
+	defaultOutpoint = &wire.OutPoint{
+		Hash:  chainhash.Hash{0x01},
+		Index: 1,
+	}
+)
+
+// TestSqlStore tests the asset swap store.
+func TestSqlStore(t *testing.T) {
+	ctxb := context.Background()
+	testDb := loopdb.NewTestDB(t)
+	defer testDb.Close()
+
+	store := NewPostgresStore(testDb)
+
+	swapPreimage := getRandomPreimage()
+	SwapHash := swapPreimage.Hash()
+
+	// Create a new SwapOut.
+	swapOut := &SwapOut{
+		SwapKit: htlc.SwapKit{
+			SwapHash:       SwapHash,
+			Amount:         100,
+			SenderPubKey:   defaultClientPubkey,
+			ReceiverPubKey: defaultClientPubkey,
+			CsvExpiry:      100,
+			AssetID:        []byte("assetid"),
+		},
+		SwapPreimage:     swapPreimage,
+		State:            fsm.StateType("init"),
+		InitiationHeight: 1,
+		ClientKeyLocator: keychain.KeyLocator{
+			Family: 1,
+			Index:  1,
+		},
+	}
+
+	// Save the swap out in the db.
+	err := store.CreateAssetSwapOut(ctxb, swapOut)
+	require.NoError(t, err)
+
+	// Insert a new swap out update.
+	err = store.InsertAssetSwapUpdate(
+		ctxb, SwapHash, fsm.StateType("state2"),
+	)
+	require.NoError(t, err)
+
+	// Try to fetch all swap outs.
+	swapOuts, err := store.GetAllAssetOuts(ctxb)
+	require.NoError(t, err)
+	require.Len(t, swapOuts, 1)
+
+	// Update the htlc outpoint.
+	err = store.UpdateAssetSwapHtlcOutpoint(
+		ctxb, SwapHash, defaultOutpoint, 100,
+	)
+	require.NoError(t, err)
+
+	// Update the offchain payment amount.
+	err = store.UpdateAssetSwapOutProof(
+		ctxb, SwapHash, []byte("proof"),
+	)
+	require.NoError(t, err)
+
+	// Try to fetch all active swap outs.
+	activeSwapOuts, err := store.GetActiveAssetOuts(ctxb)
+	require.NoError(t, err)
+	require.Len(t, activeSwapOuts, 1)
+
+	// Update the swap out state to a finished state.
+	err = store.InsertAssetSwapUpdate(
+		ctxb, SwapHash, fsm.StateType(FinishedStates()[0]),
+	)
+	require.NoError(t, err)
+
+	// Try to fetch all active swap outs.
+	activeSwapOuts, err = store.GetActiveAssetOuts(ctxb)
+	require.NoError(t, err)
+	require.Len(t, activeSwapOuts, 0)
+}
+
+// getRandomPreimage generates a random reservation ID.
+func getRandomPreimage() lntypes.Preimage {
+	var id lntypes.Preimage
+	_, err := rand.Read(id[:])
+	if err != nil {
+		panic(err)
+	}
+	return id
+}
diff --git a/assets/swap_out.go b/assets/swap_out.go
new file mode 100644
index 000000000..90a806a6f
--- /dev/null
+++ b/assets/swap_out.go
@@ -0,0 +1,106 @@
+package assets
+
+import (
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/txscript"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/loop/assets/htlc"
+	"github.com/lightninglabs/loop/fsm"
+	"github.com/lightninglabs/taproot-assets/commitment"
+	"github.com/lightninglabs/taproot-assets/proof"
+
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+// SwapOut is a struct that represents a swap out. It contains all the
+// information needed to perform a swap out.
+type SwapOut struct {
+	// We embed swapkit for all script related helpers.
+	htlc.SwapKit
+
+	// SwapPreimage is the preimage of the swap, that enables spending
+	// the success path, it's hash is the main identifier of the swap.
+	SwapPreimage lntypes.Preimage
+
+	// State is the current state of the swap.
+	State fsm.StateType
+
+	// InitiationHeight is the height at which the swap was initiated.
+	InitiationHeight uint32
+
+	// ClientKeyLocator is the key locator of the clients key.
+	ClientKeyLocator keychain.KeyLocator
+
+	// HtlcOutPoint is the outpoint of the htlc that was created to
+	// perform the swap.
+	HtlcOutPoint *wire.OutPoint
+
+	// HtlcConfirmationHeight is the height at which the htlc was
+	// confirmed.
+	HtlcConfirmationHeight uint32
+
+	// SweepOutpoint is the outpoint of the htlc that was swept.
+	SweepOutpoint *wire.OutPoint
+
+	// SweepConfirmationHeight is the height at which the sweep was
+	// confirmed.
+	SweepConfirmationHeight uint32
+
+	// SweepPkscript is the pkscript of the sweep transaction.
+	SweepPkscript []byte
+
+	// RawHtlcProof is the raw htlc proof that we need to send to the
+	// receiver. We only keep this in the OutFSM struct as we don't want
+	// to save it in the store.
+	RawHtlcProof []byte
+}
+
+// NewSwapOut creates a new swap out.
+func NewSwapOut(swapHash lntypes.Hash, amt uint64,
+	assetId []byte, clientKeyDesc *keychain.KeyDescriptor,
+	senderPubkey *btcec.PublicKey, csvExpiry, initiationHeight uint32,
+) *SwapOut {
+
+	return &SwapOut{
+		SwapKit: htlc.SwapKit{
+			SwapHash:       swapHash,
+			Amount:         amt,
+			SenderPubKey:   senderPubkey,
+			ReceiverPubKey: clientKeyDesc.PubKey,
+			CsvExpiry:      csvExpiry,
+			AssetID:        assetId,
+		},
+		State:            Init,
+		InitiationHeight: initiationHeight,
+		ClientKeyLocator: clientKeyDesc.KeyLocator,
+	}
+}
+
+// genTaprootAssetRootFromProof generates the taproot asset root from the proof
+// of the swap.
+func (s *SwapOut) genTaprootAssetRootFromProof(proof *proof.Proof) ([]byte,
+	error) {
+
+	assetCpy := proof.Asset.Copy()
+	assetCpy.PrevWitnesses[0].SplitCommitment = nil
+	sendCommitment, err := commitment.NewAssetCommitment(
+		assetCpy,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	version := commitment.TapCommitmentV2
+	assetCommitment, err := commitment.NewTapCommitment(
+		&version, sendCommitment,
+	)
+	if err != nil {
+		return nil, err
+	}
+	taprootAssetRoot := txscript.AssembleTaprootScriptTree(
+		assetCommitment.TapLeaf(),
+	).RootNode.TapHash()
+
+	return taprootAssetRoot[:], nil
+}
diff --git a/assets/tapkit.go b/assets/tapkit.go
new file mode 100644
index 000000000..8cb1b6944
--- /dev/null
+++ b/assets/tapkit.go
@@ -0,0 +1,130 @@
+package assets
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcutil/psbt"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/loop/assets/htlc"
+	"github.com/lightninglabs/taproot-assets/address"
+	"github.com/lightninglabs/taproot-assets/asset"
+	"github.com/lightninglabs/taproot-assets/commitment"
+	"github.com/lightninglabs/taproot-assets/proof"
+	"github.com/lightninglabs/taproot-assets/tappsbt"
+	"github.com/lightninglabs/taproot-assets/tapsend"
+)
+
+// GenTaprootAssetRootFromProof generates the taproot asset root from the proof
+// of the swap.
+func GenTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, error) {
+	assetCopy := proof.Asset.CopySpendTemplate()
+
+	version := commitment.TapCommitmentV2
+	assetCommitment, err := commitment.FromAssets(&version, assetCopy)
+	if err != nil {
+		return nil, err
+	}
+
+	assetCommitment, err = commitment.TrimSplitWitnesses(
+		&version, assetCommitment,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	taprootAssetRoot := assetCommitment.TapscriptRoot(nil)
+
+	return taprootAssetRoot[:], nil
+}
+
+// CreateOpTrueSweepVpkt creates a VPacket that sweeps the outputs associated
+// with the passed in proofs, given that their TAP script is a simple OP_TRUE.
+func CreateOpTrueSweepVpkt(ctx context.Context, proofs []*proof.Proof,
+	addr *address.Tap, chainParams *address.ChainParams) (
+	*tappsbt.VPacket, error) {
+
+	sweepVpkt, err := tappsbt.FromProofs(proofs, chainParams, tappsbt.V1)
+	if err != nil {
+		return nil, err
+	}
+
+	total := uint64(0)
+	for i, proof := range proofs {
+		inputKey := proof.InclusionProof.InternalKey
+
+		sweepVpkt.Inputs[i].Anchor.Bip32Derivation =
+			[]*psbt.Bip32Derivation{
+				{
+					PubKey: inputKey.SerializeCompressed(),
+				},
+			}
+		sweepVpkt.Inputs[i].Anchor.TrBip32Derivation =
+			[]*psbt.TaprootBip32Derivation{
+				{
+					XOnlyPubKey: schnorr.SerializePubKey(
+						inputKey,
+					),
+				},
+			}
+
+		total += proof.Asset.Amount
+	}
+
+	// Sanity check that the amount that we're attempting to sweep matches
+	// the address amount.
+	if total != addr.Amount {
+		return nil, fmt.Errorf("total amount of proofs does not " +
+			"match the amount of the address")
+	}
+
+	sweepVpkt.Outputs = append(sweepVpkt.Outputs, &tappsbt.VOutput{
+		AssetVersion:      addr.AssetVersion,
+		Amount:            addr.Amount,
+		Interactive:       true,
+		AnchorOutputIndex: 0,
+		ScriptKey: asset.NewScriptKey(
+			&addr.ScriptKey,
+		),
+		AnchorOutputInternalKey:      &addr.InternalKey,
+		AnchorOutputTapscriptSibling: addr.TapscriptSibling,
+		ProofDeliveryAddress:         &addr.ProofCourierAddr,
+	})
+
+	err = tapsend.PrepareOutputAssets(ctx, sweepVpkt)
+	if err != nil {
+		return nil, err
+	}
+
+	_, _, _, controlBlock, err := htlc.CreateOpTrueLeaf()
+	if err != nil {
+		return nil, err
+	}
+
+	controlBlockBytes, err := controlBlock.ToBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	opTrueScript, err := htlc.GetOpTrueScript()
+	if err != nil {
+		return nil, err
+	}
+
+	witness := wire.TxWitness{
+		opTrueScript,
+		controlBlockBytes,
+	}
+
+	firstPrevWitness := &sweepVpkt.Outputs[0].Asset.PrevWitnesses[0]
+
+	if sweepVpkt.Outputs[0].Asset.HasSplitCommitmentWitness() {
+		rootAsset := firstPrevWitness.SplitCommitment.RootAsset
+		firstPrevWitness = &rootAsset.PrevWitnesses[0]
+	}
+
+	firstPrevWitness.TxWitness = witness
+
+	return sweepVpkt, nil
+}
diff --git a/cmd/loop/assets.go b/cmd/loop/assets.go
new file mode 100644
index 000000000..e2bb2e96c
--- /dev/null
+++ b/cmd/loop/assets.go
@@ -0,0 +1,229 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"encoding/hex"
+	"errors"
+	"fmt"
+
+	"github.com/btcsuite/btcd/btcutil"
+	"github.com/lightninglabs/loop/looprpc"
+	"github.com/urfave/cli"
+)
+
+var assetsCommands = cli.Command{
+
+	Name:      "assets",
+	ShortName: "a",
+	Usage:     "manage asset swaps",
+	Description: `
+	`,
+	Subcommands: []cli.Command{
+		assetsOutCommand,
+		listOutCommand,
+		listAvailableAssetsComand,
+	},
+}
+var (
+	assetsOutCommand = cli.Command{
+		Name:      "out",
+		ShortName: "o",
+		Usage:     "swap asset out",
+		ArgsUsage: "",
+		Description: `
+		List all reservations.
+	`,
+		Flags: []cli.Flag{
+			cli.Uint64Flag{
+				Name:  "amt",
+				Usage: "the amount in satoshis to loop out.",
+			},
+			cli.StringFlag{
+				Name:  "asset_id",
+				Usage: "asset_id",
+			},
+		},
+		Action: assetSwapOut,
+	}
+	listAvailableAssetsComand = cli.Command{
+		Name:      "available",
+		ShortName: "a",
+		Usage:     "list available assets",
+		ArgsUsage: "",
+		Description: `
+		List available assets from the loop server
+	`,
+
+		Action: listAvailable,
+	}
+	listOutCommand = cli.Command{
+		Name:      "list",
+		ShortName: "l",
+		Usage:     "list asset swaps",
+		ArgsUsage: "",
+		Description: `
+		List all reservations.
+	`,
+		Action: listOut,
+	}
+)
+
+func assetSwapOut(ctx *cli.Context) error {
+	// First set up the swap client itself.
+	client, cleanup, err := getAssetsClient(ctx)
+	if err != nil {
+		return err
+	}
+	defer cleanup()
+
+	args := ctx.Args()
+
+	var amtStr string
+	switch {
+	case ctx.IsSet("amt"):
+		amtStr = ctx.String("amt")
+	case ctx.NArg() > 0:
+		amtStr = args[0]
+		args = args.Tail() //nolint: wastedassign
+	default:
+		// Show command help if no arguments and flags were provided.
+		return cli.ShowCommandHelp(ctx, "out")
+	}
+
+	amt, err := parseAmt(amtStr)
+	if err != nil {
+		return err
+	}
+	if amt <= 0 {
+		return fmt.Errorf("amount must be greater than zero")
+	}
+
+	assetId, err := hex.DecodeString(ctx.String("asset_id"))
+	if err != nil {
+		return err
+	}
+
+	if len(assetId) != 32 {
+		return fmt.Errorf("invalid asset id")
+	}
+
+	// First we'll list the available assets.
+	assets, err := client.ClientListAvailableAssets(
+		context.Background(),
+		&looprpc.ClientListAvailableAssetsRequest{},
+	)
+	if err != nil {
+		return err
+	}
+
+	// We now extract the asset name from the list of available assets.
+	var assetName string
+	for _, asset := range assets.AvailableAssets {
+		if bytes.Equal(asset.AssetId, assetId) {
+			assetName = asset.Name
+			break
+		}
+	}
+	if assetName == "" {
+		return fmt.Errorf("asset not found")
+	}
+
+	// First we'll quote the swap out to get the current fee and rate.
+	quote, err := client.ClientGetAssetSwapOutQuote(
+		context.Background(),
+		&looprpc.ClientGetAssetSwapOutQuoteRequest{
+			Amt:   uint64(amt),
+			Asset: assetId,
+		},
+	)
+	if err != nil {
+		return err
+	}
+
+	totalSats := (amt * btcutil.Amount(quote.SatsPerUnit)).MulF64(float64(1) + quote.SwapFee)
+
+	fmt.Printf(satAmtFmt, "Fixed prepay cost:", quote.PrepayAmt)
+	fmt.Printf(bpsFmt, "Swap fee:", int64(quote.SwapFee*10000))
+	fmt.Printf(satAmtFmt, "Sats per unit:", quote.SatsPerUnit)
+	fmt.Printf(satAmtFmt, "Swap Offchain payment:", totalSats)
+	fmt.Printf(satAmtFmt, "Total Send off-chain:", totalSats+btcutil.Amount(quote.PrepayAmt))
+	fmt.Printf(assetFmt, "Receive assets on-chain:", int64(amt), assetName)
+
+	fmt.Println("CONTINUE SWAP? (y/n): ")
+
+	var answer string
+	fmt.Scanln(&answer)
+	if answer != "y" {
+		return errors.New("swap canceled")
+	}
+
+	res, err := client.SwapOut(
+		context.Background(),
+		&looprpc.SwapOutRequest{
+			Amt:   uint64(amt),
+			Asset: assetId,
+		},
+	)
+	if err != nil {
+		return err
+	}
+
+	printRespJSON(res)
+	return nil
+}
+
+func listAvailable(ctx *cli.Context) error {
+	// First set up the swap client itself.
+	client, cleanup, err := getAssetsClient(ctx)
+	if err != nil {
+		return err
+	}
+	defer cleanup()
+
+	res, err := client.ClientListAvailableAssets(
+		context.Background(),
+		&looprpc.ClientListAvailableAssetsRequest{},
+	)
+	if err != nil {
+		return err
+	}
+
+	printRespJSON(res)
+	return nil
+}
+func listOut(ctx *cli.Context) error {
+	// First set up the swap client itself.
+	client, cleanup, err := getAssetsClient(ctx)
+	if err != nil {
+		return err
+	}
+	defer cleanup()
+
+	res, err := client.ListAssetSwaps(
+		context.Background(),
+		&looprpc.ListAssetSwapsRequest{},
+	)
+	if err != nil {
+		return err
+	}
+
+	printRespJSON(res)
+	return nil
+}
+
+func getAssetsClient(ctx *cli.Context) (looprpc.AssetsClientClient, func(), error) {
+	rpcServer := ctx.GlobalString("rpcserver")
+	tlsCertPath, macaroonPath, err := extractPathArgs(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
+	if err != nil {
+		return nil, nil, err
+	}
+	cleanup := func() { conn.Close() }
+
+	loopClient := looprpc.NewAssetsClientClient(conn)
+	return loopClient, cleanup, nil
+}
diff --git a/cmd/loop/main.go b/cmd/loop/main.go
index 4544dfac4..247a09bbb 100644
--- a/cmd/loop/main.go
+++ b/cmd/loop/main.go
@@ -87,7 +87,7 @@ var (
 		listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
 		setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
 		getInfoCommand, abandonSwapCommand, reservationsCommands,
-		instantOutCommand, listInstantOutsCommand,
+		instantOutCommand, listInstantOutsCommand, assetsCommands,
 	}
 )
 
@@ -114,6 +114,20 @@ const (
 	//      Exchange rate:                            0.0002 USD/SAT
 	rateFmt = "%-36s %12.4f %s/SAT\n"
 
+	// bpsFmt formats a basis point value into a one line string, intended to
+	// prettify the terminal output. For Instance,
+	// 	fmt.Printf(f, "Service fee:", fee)
+	// prints out as,
+	//      Service fee:                                    20 bps
+	bpsFmt = "%-36s %12d bps\n"
+
+	// assetFmt formats an asset into a one line string, intended to
+	// prettify the terminal output. For Instance,
+	// 	fmt.Printf(f, "Receive asset onchain:", assetName, assetAmt)
+	// prints out as,
+	//      Receive asset onchain:                       0.0001 USD
+	assetFmt = "%-36s %12d %s\n"
+
 	// blkFmt formats the number of blocks into a one line string, intended
 	// to prettify the terminal output. For Instance,
 	// 	fmt.Printf(f, "Conf target", target)
diff --git a/loopd/daemon.go b/loopd/daemon.go
index 339da0c60..06605b077 100644
--- a/loopd/daemon.go
+++ b/loopd/daemon.go
@@ -248,6 +248,8 @@ func (d *Daemon) startWebServers() error {
 	)
 	loop_looprpc.RegisterSwapClientServer(d.grpcServer, d)
 
+	loop_looprpc.RegisterAssetsClientServer(d.grpcServer, d.assetsServer)
+
 	// Register our debug server if it is compiled in.
 	d.registerDebugServer()
 
@@ -494,6 +496,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
 		swapClient.Conn,
 	)
 
+	// Create a assets server client.
+	assetsClient := loop_swaprpc.NewAssetsSwapServerClient(
+		swapClient.Conn,
+	)
+
 	// Both the client RPC server and the swap server client should stop
 	// on main context cancel. So we create it early and pass it down.
 	d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background())
@@ -643,6 +650,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
 	var (
 		reservationManager *reservation.Manager
 		instantOutManager  *instantout.Manager
+		assetManager       *assets.AssetsSwapManager
+		assetClientServer  *assets.AssetsClientServer
 	)
 
 	// Create the reservation and instantout managers.
@@ -683,6 +692,27 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
 		instantOutManager = instantout.NewInstantOutManager(
 			instantOutConfig, int32(blockHeight),
 		)
+
+		tapdClient, err := assets.NewTapdClient(
+			d.cfg.Tapd,
+		)
+		if err != nil {
+			return err
+		}
+		assetsStore := assets.NewPostgresStore(baseDb)
+		assetsConfig := &assets.Config{
+			ServerClient:         assetsClient,
+			Store:                assetsStore,
+			AssetClient:          tapdClient,
+			LndClient:            d.lnd.Client,
+			Router:               d.lnd.Router,
+			ChainNotifier:        d.lnd.ChainNotifier,
+			Signer:               d.lnd.Signer,
+			Wallet:               d.lnd.WalletKit,
+			ExchangeRateProvider: assets.NewFixedExchangeRateProvider(),
+		}
+		assetManager = assets.NewAssetSwapServer(assetsConfig)
+		assetClientServer = assets.NewAssetsServer(assetManager)
 	}
 
 	// Now finally fully initialize the swap client RPC server instance.
@@ -703,6 +733,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
 		withdrawalManager:    withdrawalManager,
 		staticLoopInManager:  staticLoopInManager,
 		assetClient:          d.assetClient,
+		assetManager:         assetManager,
+		assetsServer:         assetClientServer,
 	}
 
 	// Retrieve all currently existing swaps from the database.
@@ -897,6 +929,20 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
 		staticLoopInManager.WaitInitComplete()
 	}
 
+	// Start the asset manager.
+	if d.assetManager != nil {
+		d.wg.Add(1)
+		go func() {
+			defer d.wg.Done()
+			infof("Starting asset manager")
+			defer infof("Asset manager stopped")
+			err := d.assetManager.Run(d.mainCtx, int32(getInfo.BlockHeight))
+			if err != nil && !errors.Is(err, context.Canceled) {
+				d.internalErrChan <- err
+			}
+		}()
+	}
+
 	// Last, start our internal error handler. This will return exactly one
 	// error or nil on the main error channel to inform the caller that
 	// something went wrong or that shutdown is complete. We don't add to
@@ -942,6 +988,9 @@ func (d *Daemon) Stop() {
 
 // stop does the actual shutdown and blocks until all goroutines have exit.
 func (d *Daemon) stop() {
+	// Sleep a second in order to fix a blocking issue when having a
+	// startup error.
+	<-time.After(time.Second)
 	// First of all, we can cancel the main context that all event handlers
 	// are using. This should stop all swap activity and all event handlers
 	// should exit.
@@ -959,6 +1008,7 @@ func (d *Daemon) stop() {
 	if d.restServer != nil {
 		// Don't return the error here, we first want to give everything
 		// else a chance to shut down cleanly.
+
 		err := d.restServer.Close()
 		if err != nil {
 			errorf("Error stopping REST server: %v", err)
diff --git a/loopd/log.go b/loopd/log.go
index 0b29d4b32..1a2773720 100644
--- a/loopd/log.go
+++ b/loopd/log.go
@@ -7,6 +7,7 @@ import (
 	"github.com/lightninglabs/aperture/l402"
 	"github.com/lightninglabs/lndclient"
 	"github.com/lightninglabs/loop"
+	"github.com/lightninglabs/loop/assets"
 	"github.com/lightninglabs/loop/fsm"
 	"github.com/lightninglabs/loop/instantout"
 	"github.com/lightninglabs/loop/instantout/reservation"
@@ -16,6 +17,7 @@ import (
 	"github.com/lightninglabs/loop/staticaddr"
 	"github.com/lightninglabs/loop/sweep"
 	"github.com/lightninglabs/loop/sweepbatcher"
+	"github.com/lightninglabs/loop/utils"
 	"github.com/lightningnetwork/lnd"
 	"github.com/lightningnetwork/lnd/build"
 	"github.com/lightningnetwork/lnd/signal"
@@ -92,6 +94,13 @@ func SetupLoggers(root *build.SubLoggerManager, intercept signal.Interceptor) {
 	lnd.AddSubLogger(
 		root, sweep.Subsystem, intercept, sweep.UseLogger,
 	)
+
+	lnd.AddSubLogger(
+		root, assets.Subsystem, intercept, assets.UseLogger,
+	)
+	lnd.AddSubLogger(
+		root, utils.Subsystem, intercept, utils.UseLogger,
+	)
 }
 
 // genSubLogger creates a logger for a subsystem. We provide an instance of
diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go
index cc4a80448..f1dec54a7 100644
--- a/loopd/swapclient_server.go
+++ b/loopd/swapclient_server.go
@@ -98,6 +98,8 @@ type swapClientServer struct {
 	withdrawalManager    *withdraw.Manager
 	staticLoopInManager  *loopin.Manager
 	assetClient          *assets.TapdClient
+	assetManager         *assets.AssetsSwapManager
+	assetsServer         *assets.AssetsClientServer
 	swaps                map[lntypes.Hash]loop.SwapInfo
 	subscribers          map[int]chan<- interface{}
 	statusChan           chan loop.SwapInfo
diff --git a/loopdb/sqlc/asset_swaps.sql.go b/loopdb/sqlc/asset_swaps.sql.go
new file mode 100644
index 000000000..29ce0b4c9
--- /dev/null
+++ b/loopdb/sqlc/asset_swaps.sql.go
@@ -0,0 +1,314 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.25.0
+// source: asset_swaps.sql
+
+package sqlc
+
+import (
+	"context"
+	"time"
+)
+
+const createAssetOutSwap = `-- name: CreateAssetOutSwap :exec
+INSERT INTO asset_out_swaps (
+    swap_hash
+) VALUES (
+    $1
+)
+`
+
+func (q *Queries) CreateAssetOutSwap(ctx context.Context, swapHash []byte) error {
+	_, err := q.db.ExecContext(ctx, createAssetOutSwap, swapHash)
+	return err
+}
+
+const createAssetSwap = `-- name: CreateAssetSwap :exec
+    INSERT INTO asset_swaps(
+        swap_hash,
+        swap_preimage,
+        asset_id,
+        amt,
+        sender_pubkey,
+        receiver_pubkey,
+        csv_expiry,
+        initiation_height,
+        created_time,
+        server_key_family,
+        server_key_index
+    )
+    VALUES
+    (
+        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
+    )
+`
+
+type CreateAssetSwapParams struct {
+	SwapHash         []byte
+	SwapPreimage     []byte
+	AssetID          []byte
+	Amt              int64
+	SenderPubkey     []byte
+	ReceiverPubkey   []byte
+	CsvExpiry        int32
+	InitiationHeight int32
+	CreatedTime      time.Time
+	ServerKeyFamily  int64
+	ServerKeyIndex   int64
+}
+
+func (q *Queries) CreateAssetSwap(ctx context.Context, arg CreateAssetSwapParams) error {
+	_, err := q.db.ExecContext(ctx, createAssetSwap,
+		arg.SwapHash,
+		arg.SwapPreimage,
+		arg.AssetID,
+		arg.Amt,
+		arg.SenderPubkey,
+		arg.ReceiverPubkey,
+		arg.CsvExpiry,
+		arg.InitiationHeight,
+		arg.CreatedTime,
+		arg.ServerKeyFamily,
+		arg.ServerKeyIndex,
+	)
+	return err
+}
+
+const getAllAssetOutSwaps = `-- name: GetAllAssetOutSwaps :many
+SELECT DISTINCT
+    asw.id, asw.swap_hash, asw.swap_preimage, asw.asset_id, asw.amt, asw.sender_pubkey, asw.receiver_pubkey, asw.csv_expiry, asw.server_key_family, asw.server_key_index, asw.initiation_height, asw.created_time, asw.htlc_confirmation_height, asw.htlc_txid, asw.htlc_vout, asw.sweep_txid, asw.sweep_confirmation_height, asw.sweep_pkscript,
+    aos.swap_hash, aos.raw_proof_file,
+    asu.update_state
+FROM
+    asset_swaps asw
+INNER JOIN (
+    SELECT
+        swap_hash,
+        update_state,
+        ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn
+    FROM
+        asset_swaps_updates
+) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1
+INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash
+ORDER BY
+    asw.id
+`
+
+type GetAllAssetOutSwapsRow struct {
+	AssetSwap    AssetSwap
+	AssetOutSwap AssetOutSwap
+	UpdateState  string
+}
+
+func (q *Queries) GetAllAssetOutSwaps(ctx context.Context) ([]GetAllAssetOutSwapsRow, error) {
+	rows, err := q.db.QueryContext(ctx, getAllAssetOutSwaps)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []GetAllAssetOutSwapsRow
+	for rows.Next() {
+		var i GetAllAssetOutSwapsRow
+		if err := rows.Scan(
+			&i.AssetSwap.ID,
+			&i.AssetSwap.SwapHash,
+			&i.AssetSwap.SwapPreimage,
+			&i.AssetSwap.AssetID,
+			&i.AssetSwap.Amt,
+			&i.AssetSwap.SenderPubkey,
+			&i.AssetSwap.ReceiverPubkey,
+			&i.AssetSwap.CsvExpiry,
+			&i.AssetSwap.ServerKeyFamily,
+			&i.AssetSwap.ServerKeyIndex,
+			&i.AssetSwap.InitiationHeight,
+			&i.AssetSwap.CreatedTime,
+			&i.AssetSwap.HtlcConfirmationHeight,
+			&i.AssetSwap.HtlcTxid,
+			&i.AssetSwap.HtlcVout,
+			&i.AssetSwap.SweepTxid,
+			&i.AssetSwap.SweepConfirmationHeight,
+			&i.AssetSwap.SweepPkscript,
+			&i.AssetOutSwap.SwapHash,
+			&i.AssetOutSwap.RawProofFile,
+			&i.UpdateState,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const getAssetOutSwap = `-- name: GetAssetOutSwap :one
+SELECT DISTINCT
+    asw.id, asw.swap_hash, asw.swap_preimage, asw.asset_id, asw.amt, asw.sender_pubkey, asw.receiver_pubkey, asw.csv_expiry, asw.server_key_family, asw.server_key_index, asw.initiation_height, asw.created_time, asw.htlc_confirmation_height, asw.htlc_txid, asw.htlc_vout, asw.sweep_txid, asw.sweep_confirmation_height, asw.sweep_pkscript,
+    aos.swap_hash, aos.raw_proof_file,
+    asu.update_state
+FROM
+    asset_swaps asw
+INNER JOIN (
+    SELECT
+        swap_hash,
+        update_state,
+        ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn
+    FROM
+        asset_swaps_updates
+) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1
+INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash
+WHERE
+    asw.swap_hash = $1
+`
+
+type GetAssetOutSwapRow struct {
+	AssetSwap    AssetSwap
+	AssetOutSwap AssetOutSwap
+	UpdateState  string
+}
+
+func (q *Queries) GetAssetOutSwap(ctx context.Context, swapHash []byte) (GetAssetOutSwapRow, error) {
+	row := q.db.QueryRowContext(ctx, getAssetOutSwap, swapHash)
+	var i GetAssetOutSwapRow
+	err := row.Scan(
+		&i.AssetSwap.ID,
+		&i.AssetSwap.SwapHash,
+		&i.AssetSwap.SwapPreimage,
+		&i.AssetSwap.AssetID,
+		&i.AssetSwap.Amt,
+		&i.AssetSwap.SenderPubkey,
+		&i.AssetSwap.ReceiverPubkey,
+		&i.AssetSwap.CsvExpiry,
+		&i.AssetSwap.ServerKeyFamily,
+		&i.AssetSwap.ServerKeyIndex,
+		&i.AssetSwap.InitiationHeight,
+		&i.AssetSwap.CreatedTime,
+		&i.AssetSwap.HtlcConfirmationHeight,
+		&i.AssetSwap.HtlcTxid,
+		&i.AssetSwap.HtlcVout,
+		&i.AssetSwap.SweepTxid,
+		&i.AssetSwap.SweepConfirmationHeight,
+		&i.AssetSwap.SweepPkscript,
+		&i.AssetOutSwap.SwapHash,
+		&i.AssetOutSwap.RawProofFile,
+		&i.UpdateState,
+	)
+	return i, err
+}
+
+const insertAssetSwapUpdate = `-- name: InsertAssetSwapUpdate :exec
+INSERT INTO asset_swaps_updates (
+        swap_hash,
+        update_state,
+        update_timestamp
+) VALUES (
+        $1,
+        $2,
+        $3
+)
+`
+
+type InsertAssetSwapUpdateParams struct {
+	SwapHash        []byte
+	UpdateState     string
+	UpdateTimestamp time.Time
+}
+
+func (q *Queries) InsertAssetSwapUpdate(ctx context.Context, arg InsertAssetSwapUpdateParams) error {
+	_, err := q.db.ExecContext(ctx, insertAssetSwapUpdate, arg.SwapHash, arg.UpdateState, arg.UpdateTimestamp)
+	return err
+}
+
+const updateAssetSwapHtlcTx = `-- name: UpdateAssetSwapHtlcTx :exec
+UPDATE asset_swaps
+SET
+        htlc_confirmation_height = $2,
+        htlc_txid = $3,
+        htlc_vout = $4
+WHERE
+        asset_swaps.swap_hash = $1
+`
+
+type UpdateAssetSwapHtlcTxParams struct {
+	SwapHash               []byte
+	HtlcConfirmationHeight int32
+	HtlcTxid               []byte
+	HtlcVout               int32
+}
+
+func (q *Queries) UpdateAssetSwapHtlcTx(ctx context.Context, arg UpdateAssetSwapHtlcTxParams) error {
+	_, err := q.db.ExecContext(ctx, updateAssetSwapHtlcTx,
+		arg.SwapHash,
+		arg.HtlcConfirmationHeight,
+		arg.HtlcTxid,
+		arg.HtlcVout,
+	)
+	return err
+}
+
+const updateAssetSwapOutPreimage = `-- name: UpdateAssetSwapOutPreimage :exec
+UPDATE asset_swaps
+SET
+        swap_preimage = $2
+WHERE
+        asset_swaps.swap_hash = $1
+`
+
+type UpdateAssetSwapOutPreimageParams struct {
+	SwapHash     []byte
+	SwapPreimage []byte
+}
+
+func (q *Queries) UpdateAssetSwapOutPreimage(ctx context.Context, arg UpdateAssetSwapOutPreimageParams) error {
+	_, err := q.db.ExecContext(ctx, updateAssetSwapOutPreimage, arg.SwapHash, arg.SwapPreimage)
+	return err
+}
+
+const updateAssetSwapOutProof = `-- name: UpdateAssetSwapOutProof :exec
+UPDATE asset_out_swaps
+SET 
+        raw_proof_file = $2
+WHERE
+        asset_out_swaps.swap_hash = $1
+`
+
+type UpdateAssetSwapOutProofParams struct {
+	SwapHash     []byte
+	RawProofFile []byte
+}
+
+func (q *Queries) UpdateAssetSwapOutProof(ctx context.Context, arg UpdateAssetSwapOutProofParams) error {
+	_, err := q.db.ExecContext(ctx, updateAssetSwapOutProof, arg.SwapHash, arg.RawProofFile)
+	return err
+}
+
+const updateAssetSwapSweepTx = `-- name: UpdateAssetSwapSweepTx :exec
+UPDATE asset_swaps
+SET
+        sweep_confirmation_height = $2,
+        sweep_txid = $3,
+        sweep_pkscript = $4
+WHERE
+        asset_swaps.swap_hash = $1
+`
+
+type UpdateAssetSwapSweepTxParams struct {
+	SwapHash                []byte
+	SweepConfirmationHeight int32
+	SweepTxid               []byte
+	SweepPkscript           []byte
+}
+
+func (q *Queries) UpdateAssetSwapSweepTx(ctx context.Context, arg UpdateAssetSwapSweepTxParams) error {
+	_, err := q.db.ExecContext(ctx, updateAssetSwapSweepTx,
+		arg.SwapHash,
+		arg.SweepConfirmationHeight,
+		arg.SweepTxid,
+		arg.SweepPkscript,
+	)
+	return err
+}
diff --git a/loopdb/sqlc/migrations/000015_asset_swaps.down.sql b/loopdb/sqlc/migrations/000015_asset_swaps.down.sql
new file mode 100644
index 000000000..93b3502c2
--- /dev/null
+++ b/loopdb/sqlc/migrations/000015_asset_swaps.down.sql
@@ -0,0 +1,5 @@
+DROP INDEX IF EXISTS asset_out_swaps_swap_hash_idx;
+DROP TABLE IF EXISTS asset_out_swaps;
+DROP INDEX IF EXISTS asset_swaps_updates_swap_hash_idx;
+DROP TABLE IF EXISTS asset_swaps;
+DROP TABLE IF EXISTS asset_swaps;
\ No newline at end of file
diff --git a/loopdb/sqlc/migrations/000015_asset_swaps.up.sql b/loopdb/sqlc/migrations/000015_asset_swaps.up.sql
new file mode 100644
index 000000000..3f48e9d16
--- /dev/null
+++ b/loopdb/sqlc/migrations/000015_asset_swaps.up.sql
@@ -0,0 +1,85 @@
+CREATE TABLE IF NOT EXISTS asset_swaps (
+	--- id is the autoincrementing primary key.
+	id INTEGER PRIMARY KEY,
+
+	-- swap_hash is the randomly generated hash of the swap, which is used
+	-- as the swap identifier for the clients.
+	swap_hash BLOB NOT NULL UNIQUE,
+
+        -- swap_preimage is the preimage of the swap.
+        swap_preimage BLOB,
+
+        -- asset_id is the identifier of the asset being swapped.
+        asset_id BLOB NOT NULL,
+
+        -- amt is the requested amount to be swapped.
+        amt BIGINT NOT NULL,
+
+        -- sender_pubkey is the pubkey of the sender.
+        sender_pubkey BLOB NOT NULL,
+
+        -- receiver_pubkey is the pubkey of the receiver.
+        receiver_pubkey BLOB NOT NULL,
+
+        -- csv_expiry is the expiry of the swap.
+        csv_expiry INTEGER NOT NULL,
+
+        -- server_key_family is the family of key being identified.
+	server_key_family BIGINT NOT NULL,
+
+	-- server_key_index is the precise index of the key being identified.
+        server_key_index BIGINT NOT NULL,
+
+        -- initiation_height is the height at which the swap was initiated.
+        initiation_height INTEGER NOT NULL,
+
+        -- created_time is the time at which the swap was created.
+        created_time TIMESTAMP NOT NULL,
+
+        -- htlc_confirmation_height is the height at which the swap was confirmed.
+        htlc_confirmation_height INTEGER NOT NULL DEFAULT(0),
+
+        -- htlc_txid is the txid of the confirmation transaction.
+        htlc_txid BLOB,
+
+        -- htlc_vout is the vout of the confirmation transaction.
+        htlc_vout INTEGER NOT NULL DEFAULT (0),
+
+        -- sweep_txid is the txid of the sweep transaction.
+        sweep_txid BLOB,
+
+        -- sweep_confirmation_height is the height at which the swap was swept.
+        sweep_confirmation_height INTEGER NOT NULL DEFAULT(0),
+
+        sweep_pkscript BLOB
+);
+
+
+CREATE TABLE IF NOT EXISTS asset_swaps_updates (
+        -- id is auto incremented for each update.
+        id INTEGER PRIMARY KEY,
+
+        -- swap_hash is the hash of the swap that this update is for.
+        swap_hash BLOB NOT NULL REFERENCES asset_swaps(swap_hash),
+
+        -- update_state is the state of the swap at the time of the update.
+        update_state TEXT NOT NULL,
+
+        -- update_timestamp is the time at which the update was created.
+        update_timestamp TIMESTAMP NOT NULL
+);
+
+
+CREATE INDEX IF NOT EXISTS asset_swaps_updates_swap_hash_idx ON asset_swaps_updates(swap_hash);
+
+
+CREATE TABLE IF NOT EXISTS asset_out_swaps (
+        -- swap_hash is the identifier of the swap.
+        swap_hash BLOB PRIMARY KEY REFERENCES asset_swaps(swap_hash),
+
+       -- raw_proof_file is the file containing the raw proof.
+        raw_proof_file BLOB
+);
+
+CREATE INDEX IF NOT EXISTS asset_out_swaps_swap_hash_idx ON asset_out_swaps(swap_hash);
+
diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go
index c2e617aab..eb96fe232 100644
--- a/loopdb/sqlc/models.go
+++ b/loopdb/sqlc/models.go
@@ -9,6 +9,39 @@ import (
 	"time"
 )
 
+type AssetOutSwap struct {
+	SwapHash     []byte
+	RawProofFile []byte
+}
+
+type AssetSwap struct {
+	ID                      int32
+	SwapHash                []byte
+	SwapPreimage            []byte
+	AssetID                 []byte
+	Amt                     int64
+	SenderPubkey            []byte
+	ReceiverPubkey          []byte
+	CsvExpiry               int32
+	ServerKeyFamily         int64
+	ServerKeyIndex          int64
+	InitiationHeight        int32
+	CreatedTime             time.Time
+	HtlcConfirmationHeight  int32
+	HtlcTxid                []byte
+	HtlcVout                int32
+	SweepTxid               []byte
+	SweepConfirmationHeight int32
+	SweepPkscript           []byte
+}
+
+type AssetSwapsUpdate struct {
+	ID              int32
+	SwapHash        []byte
+	UpdateState     string
+	UpdateTimestamp time.Time
+}
+
 type Deposit struct {
 	ID                    int32
 	DepositID             []byte
diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go
index d5283b868..d55c2622c 100644
--- a/loopdb/sqlc/querier.go
+++ b/loopdb/sqlc/querier.go
@@ -13,11 +13,15 @@ type Querier interface {
 	AllDeposits(ctx context.Context) ([]Deposit, error)
 	AllStaticAddresses(ctx context.Context) ([]StaticAddress, error)
 	ConfirmBatch(ctx context.Context, id int32) error
+	CreateAssetOutSwap(ctx context.Context, swapHash []byte) error
+	CreateAssetSwap(ctx context.Context, arg CreateAssetSwapParams) error
 	CreateDeposit(ctx context.Context, arg CreateDepositParams) error
 	CreateReservation(ctx context.Context, arg CreateReservationParams) error
 	CreateStaticAddress(ctx context.Context, arg CreateStaticAddressParams) error
 	DropBatch(ctx context.Context, id int32) error
 	FetchLiquidityParams(ctx context.Context) ([]byte, error)
+	GetAllAssetOutSwaps(ctx context.Context) ([]GetAllAssetOutSwapsRow, error)
+	GetAssetOutSwap(ctx context.Context, swapHash []byte) (GetAssetOutSwapRow, error)
 	GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error)
 	GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error)
 	GetDeposit(ctx context.Context, depositID []byte) (Deposit, error)
@@ -42,6 +46,7 @@ type Querier interface {
 	GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error)
 	GetSweepStatus(ctx context.Context, outpoint string) (bool, error)
 	GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error)
+	InsertAssetSwapUpdate(ctx context.Context, arg InsertAssetSwapUpdateParams) error
 	InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error)
 	InsertDepositUpdate(ctx context.Context, arg InsertDepositUpdateParams) error
 	InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error
@@ -58,6 +63,10 @@ type Querier interface {
 	InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error
 	IsStored(ctx context.Context, swapHash []byte) (bool, error)
 	OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error
+	UpdateAssetSwapHtlcTx(ctx context.Context, arg UpdateAssetSwapHtlcTxParams) error
+	UpdateAssetSwapOutPreimage(ctx context.Context, arg UpdateAssetSwapOutPreimageParams) error
+	UpdateAssetSwapOutProof(ctx context.Context, arg UpdateAssetSwapOutProofParams) error
+	UpdateAssetSwapSweepTx(ctx context.Context, arg UpdateAssetSwapSweepTxParams) error
 	UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
 	UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error
 	UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error
diff --git a/loopdb/sqlc/queries/asset_swaps.sql b/loopdb/sqlc/queries/asset_swaps.sql
new file mode 100644
index 000000000..98201c4b4
--- /dev/null
+++ b/loopdb/sqlc/queries/asset_swaps.sql
@@ -0,0 +1,108 @@
+-- name: CreateAssetSwap :exec
+    INSERT INTO asset_swaps(
+        swap_hash,
+        swap_preimage,
+        asset_id,
+        amt,
+        sender_pubkey,
+        receiver_pubkey,
+        csv_expiry,
+        initiation_height,
+        created_time,
+        server_key_family,
+        server_key_index
+    )
+    VALUES
+    (
+        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
+    );
+
+-- name: CreateAssetOutSwap :exec
+INSERT INTO asset_out_swaps (
+    swap_hash
+) VALUES (
+    $1
+);
+
+-- name: UpdateAssetSwapHtlcTx :exec
+UPDATE asset_swaps
+SET
+        htlc_confirmation_height = $2,
+        htlc_txid = $3,
+        htlc_vout = $4
+WHERE
+        asset_swaps.swap_hash = $1;
+
+-- name: UpdateAssetSwapOutProof :exec
+UPDATE asset_out_swaps
+SET 
+        raw_proof_file = $2
+WHERE
+        asset_out_swaps.swap_hash = $1;
+
+-- name: UpdateAssetSwapOutPreimage :exec
+UPDATE asset_swaps
+SET
+        swap_preimage = $2
+WHERE
+        asset_swaps.swap_hash = $1;
+
+-- name: UpdateAssetSwapSweepTx :exec
+UPDATE asset_swaps
+SET
+        sweep_confirmation_height = $2,
+        sweep_txid = $3,
+        sweep_pkscript = $4
+WHERE
+        asset_swaps.swap_hash = $1;
+
+-- name: InsertAssetSwapUpdate :exec
+INSERT INTO asset_swaps_updates (
+        swap_hash,
+        update_state,
+        update_timestamp
+) VALUES (
+        $1,
+        $2,
+        $3
+);
+
+
+-- name: GetAssetOutSwap :one
+SELECT DISTINCT
+    sqlc.embed(asw),
+    sqlc.embed(aos),
+    asu.update_state
+FROM
+    asset_swaps asw
+INNER JOIN (
+    SELECT
+        swap_hash,
+        update_state,
+        ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn
+    FROM
+        asset_swaps_updates
+) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1
+INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash
+WHERE
+    asw.swap_hash = $1;
+
+-- name: GetAllAssetOutSwaps :many
+SELECT DISTINCT
+    sqlc.embed(asw),
+    sqlc.embed(aos),
+    asu.update_state
+FROM
+    asset_swaps asw
+INNER JOIN (
+    SELECT
+        swap_hash,
+        update_state,
+        ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn
+    FROM
+        asset_swaps_updates
+) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1
+INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash
+ORDER BY
+    asw.id;
+
diff --git a/looprpc/clientassets.pb.go b/looprpc/clientassets.pb.go
new file mode 100644
index 000000000..61bf089f1
--- /dev/null
+++ b/looprpc/clientassets.pb.go
@@ -0,0 +1,805 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.34.2
+// 	protoc        v3.21.12
+// source: clientassets.proto
+
+package looprpc
+
+import (
+	_ "github.com/lightninglabs/loop/swapserverrpc"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type SwapOutRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Amt   uint64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"`
+	Asset []byte `protobuf:"bytes,2,opt,name=asset,proto3" json:"asset,omitempty"`
+}
+
+func (x *SwapOutRequest) Reset() {
+	*x = SwapOutRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SwapOutRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SwapOutRequest) ProtoMessage() {}
+
+func (x *SwapOutRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SwapOutRequest.ProtoReflect.Descriptor instead.
+func (*SwapOutRequest) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SwapOutRequest) GetAmt() uint64 {
+	if x != nil {
+		return x.Amt
+	}
+	return 0
+}
+
+func (x *SwapOutRequest) GetAsset() []byte {
+	if x != nil {
+		return x.Asset
+	}
+	return nil
+}
+
+type SwapOutResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SwapStatus *AssetSwapStatus `protobuf:"bytes,1,opt,name=swap_status,json=swapStatus,proto3" json:"swap_status,omitempty"`
+}
+
+func (x *SwapOutResponse) Reset() {
+	*x = SwapOutResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SwapOutResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SwapOutResponse) ProtoMessage() {}
+
+func (x *SwapOutResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SwapOutResponse.ProtoReflect.Descriptor instead.
+func (*SwapOutResponse) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *SwapOutResponse) GetSwapStatus() *AssetSwapStatus {
+	if x != nil {
+		return x.SwapStatus
+	}
+	return nil
+}
+
+type ListAssetSwapsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *ListAssetSwapsRequest) Reset() {
+	*x = ListAssetSwapsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListAssetSwapsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListAssetSwapsRequest) ProtoMessage() {}
+
+func (x *ListAssetSwapsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListAssetSwapsRequest.ProtoReflect.Descriptor instead.
+func (*ListAssetSwapsRequest) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{2}
+}
+
+type ListAssetSwapsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SwapStatus []*AssetSwapStatus `protobuf:"bytes,1,rep,name=swap_status,json=swapStatus,proto3" json:"swap_status,omitempty"`
+}
+
+func (x *ListAssetSwapsResponse) Reset() {
+	*x = ListAssetSwapsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListAssetSwapsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListAssetSwapsResponse) ProtoMessage() {}
+
+func (x *ListAssetSwapsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListAssetSwapsResponse.ProtoReflect.Descriptor instead.
+func (*ListAssetSwapsResponse) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ListAssetSwapsResponse) GetSwapStatus() []*AssetSwapStatus {
+	if x != nil {
+		return x.SwapStatus
+	}
+	return nil
+}
+
+type AssetSwapStatus struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SwapHash   []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
+	SwapStatus string `protobuf:"bytes,2,opt,name=swap_status,json=swapStatus,proto3" json:"swap_status,omitempty"`
+}
+
+func (x *AssetSwapStatus) Reset() {
+	*x = AssetSwapStatus{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *AssetSwapStatus) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AssetSwapStatus) ProtoMessage() {}
+
+func (x *AssetSwapStatus) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AssetSwapStatus.ProtoReflect.Descriptor instead.
+func (*AssetSwapStatus) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *AssetSwapStatus) GetSwapHash() []byte {
+	if x != nil {
+		return x.SwapHash
+	}
+	return nil
+}
+
+func (x *AssetSwapStatus) GetSwapStatus() string {
+	if x != nil {
+		return x.SwapStatus
+	}
+	return ""
+}
+
+type ClientListAvailableAssetsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *ClientListAvailableAssetsRequest) Reset() {
+	*x = ClientListAvailableAssetsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ClientListAvailableAssetsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClientListAvailableAssetsRequest) ProtoMessage() {}
+
+func (x *ClientListAvailableAssetsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[5]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ClientListAvailableAssetsRequest.ProtoReflect.Descriptor instead.
+func (*ClientListAvailableAssetsRequest) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{5}
+}
+
+type ClientListAvailableAssetsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	AvailableAssets []*Asset `protobuf:"bytes,1,rep,name=available_assets,json=availableAssets,proto3" json:"available_assets,omitempty"`
+}
+
+func (x *ClientListAvailableAssetsResponse) Reset() {
+	*x = ClientListAvailableAssetsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ClientListAvailableAssetsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClientListAvailableAssetsResponse) ProtoMessage() {}
+
+func (x *ClientListAvailableAssetsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[6]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ClientListAvailableAssetsResponse.ProtoReflect.Descriptor instead.
+func (*ClientListAvailableAssetsResponse) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *ClientListAvailableAssetsResponse) GetAvailableAssets() []*Asset {
+	if x != nil {
+		return x.AvailableAssets
+	}
+	return nil
+}
+
+type Asset struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	AssetId     []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+	Name        string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	SatsPerUnit uint64 `protobuf:"varint,3,opt,name=sats_per_unit,json=satsPerUnit,proto3" json:"sats_per_unit,omitempty"`
+}
+
+func (x *Asset) Reset() {
+	*x = Asset{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[7]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Asset) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Asset) ProtoMessage() {}
+
+func (x *Asset) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[7]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Asset.ProtoReflect.Descriptor instead.
+func (*Asset) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *Asset) GetAssetId() []byte {
+	if x != nil {
+		return x.AssetId
+	}
+	return nil
+}
+
+func (x *Asset) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *Asset) GetSatsPerUnit() uint64 {
+	if x != nil {
+		return x.SatsPerUnit
+	}
+	return 0
+}
+
+type ClientGetAssetSwapOutQuoteRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Amt   uint64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"`
+	Asset []byte `protobuf:"bytes,2,opt,name=asset,proto3" json:"asset,omitempty"`
+}
+
+func (x *ClientGetAssetSwapOutQuoteRequest) Reset() {
+	*x = ClientGetAssetSwapOutQuoteRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ClientGetAssetSwapOutQuoteRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClientGetAssetSwapOutQuoteRequest) ProtoMessage() {}
+
+func (x *ClientGetAssetSwapOutQuoteRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[8]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ClientGetAssetSwapOutQuoteRequest.ProtoReflect.Descriptor instead.
+func (*ClientGetAssetSwapOutQuoteRequest) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *ClientGetAssetSwapOutQuoteRequest) GetAmt() uint64 {
+	if x != nil {
+		return x.Amt
+	}
+	return 0
+}
+
+func (x *ClientGetAssetSwapOutQuoteRequest) GetAsset() []byte {
+	if x != nil {
+		return x.Asset
+	}
+	return nil
+}
+
+type ClientGetAssetSwapOutQuoteResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	SwapFee     float64 `protobuf:"fixed64,1,opt,name=swap_fee,json=swapFee,proto3" json:"swap_fee,omitempty"`
+	PrepayAmt   uint64  `protobuf:"varint,2,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"`
+	SatsPerUnit uint64  `protobuf:"varint,3,opt,name=sats_per_unit,json=satsPerUnit,proto3" json:"sats_per_unit,omitempty"`
+}
+
+func (x *ClientGetAssetSwapOutQuoteResponse) Reset() {
+	*x = ClientGetAssetSwapOutQuoteResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_clientassets_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ClientGetAssetSwapOutQuoteResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ClientGetAssetSwapOutQuoteResponse) ProtoMessage() {}
+
+func (x *ClientGetAssetSwapOutQuoteResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_clientassets_proto_msgTypes[9]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ClientGetAssetSwapOutQuoteResponse.ProtoReflect.Descriptor instead.
+func (*ClientGetAssetSwapOutQuoteResponse) Descriptor() ([]byte, []int) {
+	return file_clientassets_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *ClientGetAssetSwapOutQuoteResponse) GetSwapFee() float64 {
+	if x != nil {
+		return x.SwapFee
+	}
+	return 0
+}
+
+func (x *ClientGetAssetSwapOutQuoteResponse) GetPrepayAmt() uint64 {
+	if x != nil {
+		return x.PrepayAmt
+	}
+	return 0
+}
+
+func (x *ClientGetAssetSwapOutQuoteResponse) GetSatsPerUnit() uint64 {
+	if x != nil {
+		return x.SatsPerUnit
+	}
+	return 0
+}
+
+var File_clientassets_proto protoreflect.FileDescriptor
+
+var file_clientassets_proto_rawDesc = []byte{
+	0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x1a, 0x1a, 0x73,
+	0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6d,
+	0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x38, 0x0a, 0x0e, 0x53, 0x77, 0x61,
+	0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61,
+	0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x14, 0x0a,
+	0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x73,
+	0x73, 0x65, 0x74, 0x22, 0x4c, 0x0a, 0x0f, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x73,
+	0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x6f,
+	0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x53,
+	0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x22, 0x17, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77,
+	0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x53, 0x0a, 0x16, 0x4c, 0x69,
+	0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x73, 0x74, 0x61,
+	0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70,
+	0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61,
+	0x74, 0x75, 0x73, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22,
+	0x4f, 0x0a, 0x0f, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74,
+	0x75, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12,
+	0x1f, 0x0a, 0x0b, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
+	0x22, 0x22, 0x0a, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76,
+	0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x22, 0x5e, 0x0a, 0x21, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x69,
+	0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x10, 0x61, 0x76, 0x61,
+	0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73,
+	0x73, 0x65, 0x74, 0x52, 0x0f, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73,
+	0x73, 0x65, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x05, 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x19, 0x0a,
+	0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
+	0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0d,
+	0x73, 0x61, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x03, 0x20,
+	0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x61, 0x74, 0x73, 0x50, 0x65, 0x72, 0x55, 0x6e, 0x69, 0x74,
+	0x22, 0x4b, 0x0a, 0x21, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x65, 0x74, 0x41, 0x73, 0x73,
+	0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x04, 0x52, 0x03, 0x61, 0x6d, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x22, 0x82, 0x01,
+	0x0a, 0x22, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x66, 0x65, 0x65,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x07, 0x73, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x12,
+	0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x04, 0x52, 0x09, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x41, 0x6d, 0x74, 0x12, 0x22,
+	0x0a, 0x0d, 0x73, 0x61, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x61, 0x74, 0x73, 0x50, 0x65, 0x72, 0x55, 0x6e,
+	0x69, 0x74, 0x32, 0x8a, 0x03, 0x0a, 0x0c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x43, 0x6c, 0x69,
+	0x65, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x07, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x17,
+	0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70,
+	0x63, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x51, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77,
+	0x61, 0x70, 0x73, 0x12, 0x1e, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69,
+	0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69,
+	0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x19, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x69,
+	0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x73, 0x12, 0x29, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65,
+	0x6e, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41,
+	0x73, 0x73, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6c,
+	0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x69, 0x73,
+	0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x1a, 0x43, 0x6c, 0x69, 0x65,
+	0x6e, 0x74, 0x47, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75,
+	0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
+	0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53,
+	0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x69,
+	0x65, 0x6e, 0x74, 0x47, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x4f,
+	0x75, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+	0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69,
+	0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70,
+	0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_clientassets_proto_rawDescOnce sync.Once
+	file_clientassets_proto_rawDescData = file_clientassets_proto_rawDesc
+)
+
+func file_clientassets_proto_rawDescGZIP() []byte {
+	file_clientassets_proto_rawDescOnce.Do(func() {
+		file_clientassets_proto_rawDescData = protoimpl.X.CompressGZIP(file_clientassets_proto_rawDescData)
+	})
+	return file_clientassets_proto_rawDescData
+}
+
+var file_clientassets_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
+var file_clientassets_proto_goTypes = []any{
+	(*SwapOutRequest)(nil),                     // 0: looprpc.SwapOutRequest
+	(*SwapOutResponse)(nil),                    // 1: looprpc.SwapOutResponse
+	(*ListAssetSwapsRequest)(nil),              // 2: looprpc.ListAssetSwapsRequest
+	(*ListAssetSwapsResponse)(nil),             // 3: looprpc.ListAssetSwapsResponse
+	(*AssetSwapStatus)(nil),                    // 4: looprpc.AssetSwapStatus
+	(*ClientListAvailableAssetsRequest)(nil),   // 5: looprpc.ClientListAvailableAssetsRequest
+	(*ClientListAvailableAssetsResponse)(nil),  // 6: looprpc.ClientListAvailableAssetsResponse
+	(*Asset)(nil),                              // 7: looprpc.Asset
+	(*ClientGetAssetSwapOutQuoteRequest)(nil),  // 8: looprpc.ClientGetAssetSwapOutQuoteRequest
+	(*ClientGetAssetSwapOutQuoteResponse)(nil), // 9: looprpc.ClientGetAssetSwapOutQuoteResponse
+}
+var file_clientassets_proto_depIdxs = []int32{
+	4, // 0: looprpc.SwapOutResponse.swap_status:type_name -> looprpc.AssetSwapStatus
+	4, // 1: looprpc.ListAssetSwapsResponse.swap_status:type_name -> looprpc.AssetSwapStatus
+	7, // 2: looprpc.ClientListAvailableAssetsResponse.available_assets:type_name -> looprpc.Asset
+	0, // 3: looprpc.AssetsClient.SwapOut:input_type -> looprpc.SwapOutRequest
+	2, // 4: looprpc.AssetsClient.ListAssetSwaps:input_type -> looprpc.ListAssetSwapsRequest
+	5, // 5: looprpc.AssetsClient.ClientListAvailableAssets:input_type -> looprpc.ClientListAvailableAssetsRequest
+	8, // 6: looprpc.AssetsClient.ClientGetAssetSwapOutQuote:input_type -> looprpc.ClientGetAssetSwapOutQuoteRequest
+	1, // 7: looprpc.AssetsClient.SwapOut:output_type -> looprpc.SwapOutResponse
+	3, // 8: looprpc.AssetsClient.ListAssetSwaps:output_type -> looprpc.ListAssetSwapsResponse
+	6, // 9: looprpc.AssetsClient.ClientListAvailableAssets:output_type -> looprpc.ClientListAvailableAssetsResponse
+	9, // 10: looprpc.AssetsClient.ClientGetAssetSwapOutQuote:output_type -> looprpc.ClientGetAssetSwapOutQuoteResponse
+	7, // [7:11] is the sub-list for method output_type
+	3, // [3:7] is the sub-list for method input_type
+	3, // [3:3] is the sub-list for extension type_name
+	3, // [3:3] is the sub-list for extension extendee
+	0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_clientassets_proto_init() }
+func file_clientassets_proto_init() {
+	if File_clientassets_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_clientassets_proto_msgTypes[0].Exporter = func(v any, i int) any {
+			switch v := v.(*SwapOutRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[1].Exporter = func(v any, i int) any {
+			switch v := v.(*SwapOutResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[2].Exporter = func(v any, i int) any {
+			switch v := v.(*ListAssetSwapsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[3].Exporter = func(v any, i int) any {
+			switch v := v.(*ListAssetSwapsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[4].Exporter = func(v any, i int) any {
+			switch v := v.(*AssetSwapStatus); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[5].Exporter = func(v any, i int) any {
+			switch v := v.(*ClientListAvailableAssetsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[6].Exporter = func(v any, i int) any {
+			switch v := v.(*ClientListAvailableAssetsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[7].Exporter = func(v any, i int) any {
+			switch v := v.(*Asset); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[8].Exporter = func(v any, i int) any {
+			switch v := v.(*ClientGetAssetSwapOutQuoteRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_clientassets_proto_msgTypes[9].Exporter = func(v any, i int) any {
+			switch v := v.(*ClientGetAssetSwapOutQuoteResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_clientassets_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   10,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_clientassets_proto_goTypes,
+		DependencyIndexes: file_clientassets_proto_depIdxs,
+		MessageInfos:      file_clientassets_proto_msgTypes,
+	}.Build()
+	File_clientassets_proto = out.File
+	file_clientassets_proto_rawDesc = nil
+	file_clientassets_proto_goTypes = nil
+	file_clientassets_proto_depIdxs = nil
+}
diff --git a/looprpc/clientassets.proto b/looprpc/clientassets.proto
new file mode 100644
index 000000000..25ca74717
--- /dev/null
+++ b/looprpc/clientassets.proto
@@ -0,0 +1,61 @@
+syntax = "proto3";
+
+import "swapserverrpc/common.proto";
+
+package looprpc;
+
+option go_package = "github.com/lightninglabs/loop/looprpc";
+
+service AssetsClient {
+    rpc SwapOut (SwapOutRequest) returns (SwapOutResponse);
+    rpc ListAssetSwaps (ListAssetSwapsRequest) returns (ListAssetSwapsResponse);
+    rpc ClientListAvailableAssets (ClientListAvailableAssetsRequest)
+        returns (ClientListAvailableAssetsResponse);
+    rpc ClientGetAssetSwapOutQuote (ClientGetAssetSwapOutQuoteRequest)
+        returns (ClientGetAssetSwapOutQuoteResponse);
+}
+
+message SwapOutRequest {
+    uint64 amt = 1;
+    bytes asset = 2;
+}
+
+message SwapOutResponse {
+    AssetSwapStatus swap_status = 1;
+}
+
+message ListAssetSwapsRequest {
+}
+
+message ListAssetSwapsResponse {
+    repeated AssetSwapStatus swap_status = 1;
+}
+
+message AssetSwapStatus {
+    bytes swap_hash = 1;
+    string swap_status = 2;
+}
+
+message ClientListAvailableAssetsRequest {
+}
+
+message ClientListAvailableAssetsResponse {
+    repeated Asset available_assets = 1;
+}
+
+message Asset {
+    bytes asset_id = 1;
+    string name = 2;
+    uint64 sats_per_unit = 3;
+}
+
+message ClientGetAssetSwapOutQuoteRequest {
+    uint64 amt = 1;
+    bytes asset = 2;
+}
+
+message ClientGetAssetSwapOutQuoteResponse {
+    double swap_fee = 1;
+    uint64 prepay_amt = 2;
+    uint64 sats_per_unit = 3;
+}
\ No newline at end of file
diff --git a/looprpc/clientassets_grpc.pb.go b/looprpc/clientassets_grpc.pb.go
new file mode 100644
index 000000000..129381600
--- /dev/null
+++ b/looprpc/clientassets_grpc.pb.go
@@ -0,0 +1,209 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package looprpc
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// AssetsClientClient is the client API for AssetsClient service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type AssetsClientClient interface {
+	SwapOut(ctx context.Context, in *SwapOutRequest, opts ...grpc.CallOption) (*SwapOutResponse, error)
+	ListAssetSwaps(ctx context.Context, in *ListAssetSwapsRequest, opts ...grpc.CallOption) (*ListAssetSwapsResponse, error)
+	ClientListAvailableAssets(ctx context.Context, in *ClientListAvailableAssetsRequest, opts ...grpc.CallOption) (*ClientListAvailableAssetsResponse, error)
+	ClientGetAssetSwapOutQuote(ctx context.Context, in *ClientGetAssetSwapOutQuoteRequest, opts ...grpc.CallOption) (*ClientGetAssetSwapOutQuoteResponse, error)
+}
+
+type assetsClientClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewAssetsClientClient(cc grpc.ClientConnInterface) AssetsClientClient {
+	return &assetsClientClient{cc}
+}
+
+func (c *assetsClientClient) SwapOut(ctx context.Context, in *SwapOutRequest, opts ...grpc.CallOption) (*SwapOutResponse, error) {
+	out := new(SwapOutResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsClient/SwapOut", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsClientClient) ListAssetSwaps(ctx context.Context, in *ListAssetSwapsRequest, opts ...grpc.CallOption) (*ListAssetSwapsResponse, error) {
+	out := new(ListAssetSwapsResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsClient/ListAssetSwaps", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsClientClient) ClientListAvailableAssets(ctx context.Context, in *ClientListAvailableAssetsRequest, opts ...grpc.CallOption) (*ClientListAvailableAssetsResponse, error) {
+	out := new(ClientListAvailableAssetsResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsClient/ClientListAvailableAssets", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsClientClient) ClientGetAssetSwapOutQuote(ctx context.Context, in *ClientGetAssetSwapOutQuoteRequest, opts ...grpc.CallOption) (*ClientGetAssetSwapOutQuoteResponse, error) {
+	out := new(ClientGetAssetSwapOutQuoteResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsClient/ClientGetAssetSwapOutQuote", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// AssetsClientServer is the server API for AssetsClient service.
+// All implementations must embed UnimplementedAssetsClientServer
+// for forward compatibility
+type AssetsClientServer interface {
+	SwapOut(context.Context, *SwapOutRequest) (*SwapOutResponse, error)
+	ListAssetSwaps(context.Context, *ListAssetSwapsRequest) (*ListAssetSwapsResponse, error)
+	ClientListAvailableAssets(context.Context, *ClientListAvailableAssetsRequest) (*ClientListAvailableAssetsResponse, error)
+	ClientGetAssetSwapOutQuote(context.Context, *ClientGetAssetSwapOutQuoteRequest) (*ClientGetAssetSwapOutQuoteResponse, error)
+	mustEmbedUnimplementedAssetsClientServer()
+}
+
+// UnimplementedAssetsClientServer must be embedded to have forward compatible implementations.
+type UnimplementedAssetsClientServer struct {
+}
+
+func (UnimplementedAssetsClientServer) SwapOut(context.Context, *SwapOutRequest) (*SwapOutResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method SwapOut not implemented")
+}
+func (UnimplementedAssetsClientServer) ListAssetSwaps(context.Context, *ListAssetSwapsRequest) (*ListAssetSwapsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ListAssetSwaps not implemented")
+}
+func (UnimplementedAssetsClientServer) ClientListAvailableAssets(context.Context, *ClientListAvailableAssetsRequest) (*ClientListAvailableAssetsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ClientListAvailableAssets not implemented")
+}
+func (UnimplementedAssetsClientServer) ClientGetAssetSwapOutQuote(context.Context, *ClientGetAssetSwapOutQuoteRequest) (*ClientGetAssetSwapOutQuoteResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ClientGetAssetSwapOutQuote not implemented")
+}
+func (UnimplementedAssetsClientServer) mustEmbedUnimplementedAssetsClientServer() {}
+
+// UnsafeAssetsClientServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to AssetsClientServer will
+// result in compilation errors.
+type UnsafeAssetsClientServer interface {
+	mustEmbedUnimplementedAssetsClientServer()
+}
+
+func RegisterAssetsClientServer(s grpc.ServiceRegistrar, srv AssetsClientServer) {
+	s.RegisterService(&AssetsClient_ServiceDesc, srv)
+}
+
+func _AssetsClient_SwapOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SwapOutRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsClientServer).SwapOut(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsClient/SwapOut",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsClientServer).SwapOut(ctx, req.(*SwapOutRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsClient_ListAssetSwaps_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListAssetSwapsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsClientServer).ListAssetSwaps(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsClient/ListAssetSwaps",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsClientServer).ListAssetSwaps(ctx, req.(*ListAssetSwapsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsClient_ClientListAvailableAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ClientListAvailableAssetsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsClientServer).ClientListAvailableAssets(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsClient/ClientListAvailableAssets",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsClientServer).ClientListAvailableAssets(ctx, req.(*ClientListAvailableAssetsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsClient_ClientGetAssetSwapOutQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ClientGetAssetSwapOutQuoteRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsClientServer).ClientGetAssetSwapOutQuote(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsClient/ClientGetAssetSwapOutQuote",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsClientServer).ClientGetAssetSwapOutQuote(ctx, req.(*ClientGetAssetSwapOutQuoteRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// AssetsClient_ServiceDesc is the grpc.ServiceDesc for AssetsClient service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var AssetsClient_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "looprpc.AssetsClient",
+	HandlerType: (*AssetsClientServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "SwapOut",
+			Handler:    _AssetsClient_SwapOut_Handler,
+		},
+		{
+			MethodName: "ListAssetSwaps",
+			Handler:    _AssetsClient_ListAssetSwaps_Handler,
+		},
+		{
+			MethodName: "ClientListAvailableAssets",
+			Handler:    _AssetsClient_ClientListAvailableAssets_Handler,
+		},
+		{
+			MethodName: "ClientGetAssetSwapOutQuote",
+			Handler:    _AssetsClient_ClientGetAssetSwapOutQuote_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "clientassets.proto",
+}
diff --git a/looprpc/perms.go b/looprpc/perms.go
index 1082cbb73..10ba1e97a 100644
--- a/looprpc/perms.go
+++ b/looprpc/perms.go
@@ -169,4 +169,20 @@ var RequiredPermissions = map[string][]bakery.Op{
 		Entity: "swap",
 		Action: "read",
 	}},
+	"/looprpc.AssetsClient/SwapOut": {{
+		Entity: "swap",
+		Action: "execute",
+	}},
+	"/looprpc.AssetsClient/ListAssetSwaps": {{
+		Entity: "swap",
+		Action: "read",
+	}},
+	"/looprpc.AssetsClient/ClientListAvailableAssets": {{
+		Entity: "swap",
+		Action: "read",
+	}},
+	"/looprpc.AssetsClient/ClientGetAssetSwapOutQuote": {{
+		Entity: "swap",
+		Action: "read",
+	}},
 }
diff --git a/swapserverrpc/assets.pb.go b/swapserverrpc/assets.pb.go
new file mode 100644
index 000000000..53daa7a5b
--- /dev/null
+++ b/swapserverrpc/assets.pb.go
@@ -0,0 +1,1114 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
+// source: assets.proto
+
+// We can't change this to swapserverrpc, it would be a breaking change because
+// the package name is also contained in the HTTP URIs and old clients would
+// call the wrong endpoints. Luckily with the go_package option we can have
+// different golang and RPC package names to fix protobuf namespace conflicts.
+
+package swapserverrpc
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type QuoteAssetLoopOutRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// amount is the amount of the asset to loop out.
+	Amount uint64 `protobuf:"varint,1,opt,name=amount,proto3" json:"amount,omitempty"`
+	// asset is the asset to loop out.
+	Asset []byte `protobuf:"bytes,2,opt,name=asset,proto3" json:"asset,omitempty"`
+}
+
+func (x *QuoteAssetLoopOutRequest) Reset() {
+	*x = QuoteAssetLoopOutRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *QuoteAssetLoopOutRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*QuoteAssetLoopOutRequest) ProtoMessage() {}
+
+func (x *QuoteAssetLoopOutRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use QuoteAssetLoopOutRequest.ProtoReflect.Descriptor instead.
+func (*QuoteAssetLoopOutRequest) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *QuoteAssetLoopOutRequest) GetAmount() uint64 {
+	if x != nil {
+		return x.Amount
+	}
+	return 0
+}
+
+func (x *QuoteAssetLoopOutRequest) GetAsset() []byte {
+	if x != nil {
+		return x.Asset
+	}
+	return nil
+}
+
+type QuoteAssetLoopOutResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// fixed_prepay_amt is the fixed prepay amount of the swap.
+	FixedPrepayAmt uint64 `protobuf:"varint,1,opt,name=fixed_prepay_amt,json=fixedPrepayAmt,proto3" json:"fixed_prepay_amt,omitempty"`
+	// swap_fee_rate is the fee rate that is added to the swap invoice.
+	SwapFeeRate float64 `protobuf:"fixed64,2,opt,name=swap_fee_rate,json=swapFeeRate,proto3" json:"swap_fee_rate,omitempty"`
+	// current_sats_per_asset_unit is the sats per asset unit of the swap.
+	CurrentSatsPerAssetUnit uint64 `protobuf:"varint,3,opt,name=current_sats_per_asset_unit,json=currentSatsPerAssetUnit,proto3" json:"current_sats_per_asset_unit,omitempty"`
+}
+
+func (x *QuoteAssetLoopOutResponse) Reset() {
+	*x = QuoteAssetLoopOutResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *QuoteAssetLoopOutResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*QuoteAssetLoopOutResponse) ProtoMessage() {}
+
+func (x *QuoteAssetLoopOutResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use QuoteAssetLoopOutResponse.ProtoReflect.Descriptor instead.
+func (*QuoteAssetLoopOutResponse) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *QuoteAssetLoopOutResponse) GetFixedPrepayAmt() uint64 {
+	if x != nil {
+		return x.FixedPrepayAmt
+	}
+	return 0
+}
+
+func (x *QuoteAssetLoopOutResponse) GetSwapFeeRate() float64 {
+	if x != nil {
+		return x.SwapFeeRate
+	}
+	return 0
+}
+
+func (x *QuoteAssetLoopOutResponse) GetCurrentSatsPerAssetUnit() uint64 {
+	if x != nil {
+		return x.CurrentSatsPerAssetUnit
+	}
+	return 0
+}
+
+type ListAvailableAssetsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *ListAvailableAssetsRequest) Reset() {
+	*x = ListAvailableAssetsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListAvailableAssetsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListAvailableAssetsRequest) ProtoMessage() {}
+
+func (x *ListAvailableAssetsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListAvailableAssetsRequest.ProtoReflect.Descriptor instead.
+func (*ListAvailableAssetsRequest) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{2}
+}
+
+type ListAvailableAssetsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// assets is the list of assets that the server supports.
+	Assets []*AssetInfo `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
+}
+
+func (x *ListAvailableAssetsResponse) Reset() {
+	*x = ListAvailableAssetsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListAvailableAssetsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListAvailableAssetsResponse) ProtoMessage() {}
+
+func (x *ListAvailableAssetsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListAvailableAssetsResponse.ProtoReflect.Descriptor instead.
+func (*ListAvailableAssetsResponse) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *ListAvailableAssetsResponse) GetAssets() []*AssetInfo {
+	if x != nil {
+		return x.Assets
+	}
+	return nil
+}
+
+type AssetInfo struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	AssetId                 []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"`
+	CurrentSatsPerAssetUnit uint64 `protobuf:"varint,2,opt,name=current_sats_per_asset_unit,json=currentSatsPerAssetUnit,proto3" json:"current_sats_per_asset_unit,omitempty"`
+}
+
+func (x *AssetInfo) Reset() {
+	*x = AssetInfo{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *AssetInfo) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AssetInfo) ProtoMessage() {}
+
+func (x *AssetInfo) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AssetInfo.ProtoReflect.Descriptor instead.
+func (*AssetInfo) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *AssetInfo) GetAssetId() []byte {
+	if x != nil {
+		return x.AssetId
+	}
+	return nil
+}
+
+func (x *AssetInfo) GetCurrentSatsPerAssetUnit() uint64 {
+	if x != nil {
+		return x.CurrentSatsPerAssetUnit
+	}
+	return 0
+}
+
+type RequestAssetLoopOutRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// amount is the amount of the asset to loop out.
+	Amount uint64 `protobuf:"varint,1,opt,name=amount,proto3" json:"amount,omitempty"`
+	// receiver_key is the public key of the receiver.
+	ReceiverKey []byte `protobuf:"bytes,2,opt,name=receiver_key,json=receiverKey,proto3" json:"receiver_key,omitempty"`
+	// requested_asset is the asset to loop out.
+	RequestedAsset []byte `protobuf:"bytes,3,opt,name=requested_asset,json=requestedAsset,proto3" json:"requested_asset,omitempty"`
+}
+
+func (x *RequestAssetLoopOutRequest) Reset() {
+	*x = RequestAssetLoopOutRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RequestAssetLoopOutRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestAssetLoopOutRequest) ProtoMessage() {}
+
+func (x *RequestAssetLoopOutRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[5]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RequestAssetLoopOutRequest.ProtoReflect.Descriptor instead.
+func (*RequestAssetLoopOutRequest) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *RequestAssetLoopOutRequest) GetAmount() uint64 {
+	if x != nil {
+		return x.Amount
+	}
+	return 0
+}
+
+func (x *RequestAssetLoopOutRequest) GetReceiverKey() []byte {
+	if x != nil {
+		return x.ReceiverKey
+	}
+	return nil
+}
+
+func (x *RequestAssetLoopOutRequest) GetRequestedAsset() []byte {
+	if x != nil {
+		return x.RequestedAsset
+	}
+	return nil
+}
+
+type RequestAssetLoopOutResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// swap_hash is the main identifier of the swap.
+	SwapHash []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
+	// prepay_invoice is the invoice to pay to start the swap. On accepted
+	// the server will publish the htlc output.
+	PrepayInvoice string `protobuf:"bytes,2,opt,name=prepay_invoice,json=prepayInvoice,proto3" json:"prepay_invoice,omitempty"`
+	// expiry is the expiry of the htlc output.
+	Expiry int64 `protobuf:"varint,3,opt,name=expiry,proto3" json:"expiry,omitempty"`
+	// sender_pubkey is the public key of the sender.
+	SenderPubkey []byte `protobuf:"bytes,4,opt,name=sender_pubkey,json=senderPubkey,proto3" json:"sender_pubkey,omitempty"`
+}
+
+func (x *RequestAssetLoopOutResponse) Reset() {
+	*x = RequestAssetLoopOutResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[6]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RequestAssetLoopOutResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestAssetLoopOutResponse) ProtoMessage() {}
+
+func (x *RequestAssetLoopOutResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[6]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RequestAssetLoopOutResponse.ProtoReflect.Descriptor instead.
+func (*RequestAssetLoopOutResponse) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *RequestAssetLoopOutResponse) GetSwapHash() []byte {
+	if x != nil {
+		return x.SwapHash
+	}
+	return nil
+}
+
+func (x *RequestAssetLoopOutResponse) GetPrepayInvoice() string {
+	if x != nil {
+		return x.PrepayInvoice
+	}
+	return ""
+}
+
+func (x *RequestAssetLoopOutResponse) GetExpiry() int64 {
+	if x != nil {
+		return x.Expiry
+	}
+	return 0
+}
+
+func (x *RequestAssetLoopOutResponse) GetSenderPubkey() []byte {
+	if x != nil {
+		return x.SenderPubkey
+	}
+	return nil
+}
+
+type PollAssetLoopOutProofRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// swap_hash is the main identifier of the swap.
+	SwapHash []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
+}
+
+func (x *PollAssetLoopOutProofRequest) Reset() {
+	*x = PollAssetLoopOutProofRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[7]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PollAssetLoopOutProofRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PollAssetLoopOutProofRequest) ProtoMessage() {}
+
+func (x *PollAssetLoopOutProofRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[7]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PollAssetLoopOutProofRequest.ProtoReflect.Descriptor instead.
+func (*PollAssetLoopOutProofRequest) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *PollAssetLoopOutProofRequest) GetSwapHash() []byte {
+	if x != nil {
+		return x.SwapHash
+	}
+	return nil
+}
+
+type PollAssetLoopOutProofResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// raw_proof_file is the raw proof file of the swap.
+	RawProofFile []byte `protobuf:"bytes,1,opt,name=raw_proof_file,json=rawProofFile,proto3" json:"raw_proof_file,omitempty"`
+}
+
+func (x *PollAssetLoopOutProofResponse) Reset() {
+	*x = PollAssetLoopOutProofResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[8]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PollAssetLoopOutProofResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PollAssetLoopOutProofResponse) ProtoMessage() {}
+
+func (x *PollAssetLoopOutProofResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[8]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PollAssetLoopOutProofResponse.ProtoReflect.Descriptor instead.
+func (*PollAssetLoopOutProofResponse) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *PollAssetLoopOutProofResponse) GetRawProofFile() []byte {
+	if x != nil {
+		return x.RawProofFile
+	}
+	return nil
+}
+
+type RequestAssetBuyRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// swap_hash is the main identifier of the swap.
+	SwapHash []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
+}
+
+func (x *RequestAssetBuyRequest) Reset() {
+	*x = RequestAssetBuyRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RequestAssetBuyRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestAssetBuyRequest) ProtoMessage() {}
+
+func (x *RequestAssetBuyRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[9]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RequestAssetBuyRequest.ProtoReflect.Descriptor instead.
+func (*RequestAssetBuyRequest) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *RequestAssetBuyRequest) GetSwapHash() []byte {
+	if x != nil {
+		return x.SwapHash
+	}
+	return nil
+}
+
+type RequestAssetBuyResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// swap_invoice is the invoice to pay to receive the preimage to claim the
+	// asset.
+	SwapInvoice string `protobuf:"bytes,1,opt,name=swap_invoice,json=swapInvoice,proto3" json:"swap_invoice,omitempty"`
+}
+
+func (x *RequestAssetBuyResponse) Reset() {
+	*x = RequestAssetBuyResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[10]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RequestAssetBuyResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestAssetBuyResponse) ProtoMessage() {}
+
+func (x *RequestAssetBuyResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[10]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RequestAssetBuyResponse.ProtoReflect.Descriptor instead.
+func (*RequestAssetBuyResponse) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *RequestAssetBuyResponse) GetSwapInvoice() string {
+	if x != nil {
+		return x.SwapInvoice
+	}
+	return ""
+}
+
+type RequestMusig2SweepRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// swap_hash is the main identifier of the swap.
+	SwapHash []byte `protobuf:"bytes,1,opt,name=swap_hash,json=swapHash,proto3" json:"swap_hash,omitempty"`
+	// digest is that the client wants the server to sign.
+	Digest []byte `protobuf:"bytes,2,opt,name=digest,proto3" json:"digest,omitempty"`
+	// receiver_nonce is the nonce of the receiver.
+	ReceiverNonce []byte `protobuf:"bytes,3,opt,name=receiver_nonce,json=receiverNonce,proto3" json:"receiver_nonce,omitempty"`
+	// roothash is the roothash we tweak the musig2 pubkey with.
+	Roothash []byte `protobuf:"bytes,4,opt,name=roothash,proto3" json:"roothash,omitempty"`
+}
+
+func (x *RequestMusig2SweepRequest) Reset() {
+	*x = RequestMusig2SweepRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[11]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RequestMusig2SweepRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestMusig2SweepRequest) ProtoMessage() {}
+
+func (x *RequestMusig2SweepRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[11]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RequestMusig2SweepRequest.ProtoReflect.Descriptor instead.
+func (*RequestMusig2SweepRequest) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *RequestMusig2SweepRequest) GetSwapHash() []byte {
+	if x != nil {
+		return x.SwapHash
+	}
+	return nil
+}
+
+func (x *RequestMusig2SweepRequest) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+func (x *RequestMusig2SweepRequest) GetReceiverNonce() []byte {
+	if x != nil {
+		return x.ReceiverNonce
+	}
+	return nil
+}
+
+func (x *RequestMusig2SweepRequest) GetRoothash() []byte {
+	if x != nil {
+		return x.Roothash
+	}
+	return nil
+}
+
+type RequestMusig2SweepResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// sender_nonce is the nonce of the sender.
+	SenderNonce []byte `protobuf:"bytes,1,opt,name=sender_nonce,json=senderNonce,proto3" json:"sender_nonce,omitempty"`
+	// sender_sig is the signature of the sender.
+	SenderSig []byte `protobuf:"bytes,2,opt,name=sender_sig,json=senderSig,proto3" json:"sender_sig,omitempty"`
+}
+
+func (x *RequestMusig2SweepResponse) Reset() {
+	*x = RequestMusig2SweepResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_assets_proto_msgTypes[12]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RequestMusig2SweepResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RequestMusig2SweepResponse) ProtoMessage() {}
+
+func (x *RequestMusig2SweepResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_assets_proto_msgTypes[12]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RequestMusig2SweepResponse.ProtoReflect.Descriptor instead.
+func (*RequestMusig2SweepResponse) Descriptor() ([]byte, []int) {
+	return file_assets_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *RequestMusig2SweepResponse) GetSenderNonce() []byte {
+	if x != nil {
+		return x.SenderNonce
+	}
+	return nil
+}
+
+func (x *RequestMusig2SweepResponse) GetSenderSig() []byte {
+	if x != nil {
+		return x.SenderSig
+	}
+	return nil
+}
+
+var File_assets_proto protoreflect.FileDescriptor
+
+var file_assets_proto_rawDesc = []byte{
+	0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07,
+	0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x22, 0x48, 0x0a, 0x18, 0x51, 0x75, 0x6f, 0x74, 0x65,
+	0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61,
+	0x73, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65,
+	0x74, 0x22, 0xa7, 0x01, 0x0a, 0x19, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+	0x28, 0x0a, 0x10, 0x66, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x5f,
+	0x61, 0x6d, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x66, 0x69, 0x78, 0x65, 0x64,
+	0x50, 0x72, 0x65, 0x70, 0x61, 0x79, 0x41, 0x6d, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x77, 0x61,
+	0x70, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01,
+	0x52, 0x0b, 0x73, 0x77, 0x61, 0x70, 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a,
+	0x1b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x73, 0x5f, 0x70, 0x65,
+	0x72, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x04, 0x52, 0x17, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x61, 0x74, 0x73, 0x50,
+	0x65, 0x72, 0x41, 0x73, 0x73, 0x65, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x22, 0x1c, 0x0a, 0x1a, 0x4c,
+	0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73, 0x65,
+	0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x49, 0x0a, 0x1b, 0x4c, 0x69, 0x73,
+	0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x73, 0x73, 0x65,
+	0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72,
+	0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x61, 0x73,
+	0x73, 0x65, 0x74, 0x73, 0x22, 0x64, 0x0a, 0x09, 0x41, 0x73, 0x73, 0x65, 0x74, 0x49, 0x6e, 0x66,
+	0x6f, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b,
+	0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72,
+	0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x04, 0x52, 0x17, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x61, 0x74, 0x73, 0x50, 0x65,
+	0x72, 0x41, 0x73, 0x73, 0x65, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x22, 0x80, 0x01, 0x0a, 0x1a, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x4f,
+	0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f,
+	0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e,
+	0x74, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65,
+	0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65,
+	0x72, 0x4b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65,
+	0x64, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x72,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x22, 0x9e, 0x01,
+	0x0a, 0x1b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f,
+	0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a,
+	0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72,
+	0x65, 0x70, 0x61, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x70, 0x61, 0x79, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63,
+	0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x03, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x6e,
+	0x64, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x3b,
+	0x0a, 0x1c, 0x50, 0x6f, 0x6c, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x4f,
+	0x75, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b,
+	0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x22, 0x45, 0x0a, 0x1d, 0x50,
+	0x6f, 0x6c, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50,
+	0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0e,
+	0x72, 0x61, 0x77, 0x5f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x61, 0x77, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x46, 0x69,
+	0x6c, 0x65, 0x22, 0x35, 0x0a, 0x16, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73,
+	0x65, 0x74, 0x42, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09,
+	0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
+	0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61, 0x73, 0x68, 0x22, 0x3c, 0x0a, 0x17, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x69, 0x6e, 0x76,
+	0x6f, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x77, 0x61, 0x70,
+	0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x67, 0x32, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x68, 0x61,
+	0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x48, 0x61,
+	0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65,
+	0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63,
+	0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x6f, 0x74, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x6f, 0x6f, 0x74, 0x68, 0x61, 0x73, 0x68, 0x22, 0x5e, 0x0a,
+	0x1a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x67, 0x32, 0x53, 0x77,
+	0x65, 0x65, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73,
+	0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x0c, 0x52, 0x0b, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1d,
+	0x0a, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x53, 0x69, 0x67, 0x32, 0xcf, 0x04,
+	0x0a, 0x10, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x53, 0x77, 0x61, 0x70, 0x53, 0x65, 0x72, 0x76,
+	0x65, 0x72, 0x12, 0x5a, 0x0a, 0x11, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x21, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70,
+	0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70,
+	0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6c, 0x6f, 0x6f,
+	0x70, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c,
+	0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60,
+	0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41,
+	0x73, 0x73, 0x65, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e,
+	0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x73, 0x73,
+	0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6c, 0x6f, 0x6f,
+	0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62,
+	0x6c, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x12, 0x60, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74,
+	0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x12, 0x23, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70,
+	0x63, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f,
+	0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6c,
+	0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73,
+	0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x66, 0x0a, 0x15, 0x50, 0x6f, 0x6c, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c,
+	0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x25, 0x2e, 0x6c, 0x6f,
+	0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c,
+	0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c,
+	0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4c, 0x6f, 0x6f, 0x70, 0x4f, 0x75, 0x74, 0x50, 0x72, 0x6f,
+	0x6f, 0x66, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x12, 0x1f, 0x2e,
+	0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x41,
+	0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20,
+	0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x12, 0x5d, 0x0a, 0x12, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x67,
+	0x32, 0x53, 0x77, 0x65, 0x65, 0x70, 0x12, 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
+	0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x67, 0x32, 0x53, 0x77,
+	0x65, 0x65, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x6f, 0x6f,
+	0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x75, 0x73, 0x69,
+	0x67, 0x32, 0x53, 0x77, 0x65, 0x65, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
+	0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69,
+	0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70,
+	0x2f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_assets_proto_rawDescOnce sync.Once
+	file_assets_proto_rawDescData = file_assets_proto_rawDesc
+)
+
+func file_assets_proto_rawDescGZIP() []byte {
+	file_assets_proto_rawDescOnce.Do(func() {
+		file_assets_proto_rawDescData = protoimpl.X.CompressGZIP(file_assets_proto_rawDescData)
+	})
+	return file_assets_proto_rawDescData
+}
+
+var file_assets_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
+var file_assets_proto_goTypes = []interface{}{
+	(*QuoteAssetLoopOutRequest)(nil),      // 0: looprpc.QuoteAssetLoopOutRequest
+	(*QuoteAssetLoopOutResponse)(nil),     // 1: looprpc.QuoteAssetLoopOutResponse
+	(*ListAvailableAssetsRequest)(nil),    // 2: looprpc.ListAvailableAssetsRequest
+	(*ListAvailableAssetsResponse)(nil),   // 3: looprpc.ListAvailableAssetsResponse
+	(*AssetInfo)(nil),                     // 4: looprpc.AssetInfo
+	(*RequestAssetLoopOutRequest)(nil),    // 5: looprpc.RequestAssetLoopOutRequest
+	(*RequestAssetLoopOutResponse)(nil),   // 6: looprpc.RequestAssetLoopOutResponse
+	(*PollAssetLoopOutProofRequest)(nil),  // 7: looprpc.PollAssetLoopOutProofRequest
+	(*PollAssetLoopOutProofResponse)(nil), // 8: looprpc.PollAssetLoopOutProofResponse
+	(*RequestAssetBuyRequest)(nil),        // 9: looprpc.RequestAssetBuyRequest
+	(*RequestAssetBuyResponse)(nil),       // 10: looprpc.RequestAssetBuyResponse
+	(*RequestMusig2SweepRequest)(nil),     // 11: looprpc.RequestMusig2SweepRequest
+	(*RequestMusig2SweepResponse)(nil),    // 12: looprpc.RequestMusig2SweepResponse
+}
+var file_assets_proto_depIdxs = []int32{
+	4,  // 0: looprpc.ListAvailableAssetsResponse.assets:type_name -> looprpc.AssetInfo
+	0,  // 1: looprpc.AssetsSwapServer.QuoteAssetLoopOut:input_type -> looprpc.QuoteAssetLoopOutRequest
+	2,  // 2: looprpc.AssetsSwapServer.ListAvailableAssets:input_type -> looprpc.ListAvailableAssetsRequest
+	5,  // 3: looprpc.AssetsSwapServer.RequestAssetLoopOut:input_type -> looprpc.RequestAssetLoopOutRequest
+	7,  // 4: looprpc.AssetsSwapServer.PollAssetLoopOutProof:input_type -> looprpc.PollAssetLoopOutProofRequest
+	9,  // 5: looprpc.AssetsSwapServer.RequestAssetBuy:input_type -> looprpc.RequestAssetBuyRequest
+	11, // 6: looprpc.AssetsSwapServer.RequestMusig2Sweep:input_type -> looprpc.RequestMusig2SweepRequest
+	1,  // 7: looprpc.AssetsSwapServer.QuoteAssetLoopOut:output_type -> looprpc.QuoteAssetLoopOutResponse
+	3,  // 8: looprpc.AssetsSwapServer.ListAvailableAssets:output_type -> looprpc.ListAvailableAssetsResponse
+	6,  // 9: looprpc.AssetsSwapServer.RequestAssetLoopOut:output_type -> looprpc.RequestAssetLoopOutResponse
+	8,  // 10: looprpc.AssetsSwapServer.PollAssetLoopOutProof:output_type -> looprpc.PollAssetLoopOutProofResponse
+	10, // 11: looprpc.AssetsSwapServer.RequestAssetBuy:output_type -> looprpc.RequestAssetBuyResponse
+	12, // 12: looprpc.AssetsSwapServer.RequestMusig2Sweep:output_type -> looprpc.RequestMusig2SweepResponse
+	7,  // [7:13] is the sub-list for method output_type
+	1,  // [1:7] is the sub-list for method input_type
+	1,  // [1:1] is the sub-list for extension type_name
+	1,  // [1:1] is the sub-list for extension extendee
+	0,  // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_assets_proto_init() }
+func file_assets_proto_init() {
+	if File_assets_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_assets_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*QuoteAssetLoopOutRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*QuoteAssetLoopOutResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListAvailableAssetsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListAvailableAssetsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*AssetInfo); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RequestAssetLoopOutRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RequestAssetLoopOutResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PollAssetLoopOutProofRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PollAssetLoopOutProofResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RequestAssetBuyRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RequestAssetBuyResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RequestMusig2SweepRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_assets_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RequestMusig2SweepResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_assets_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   13,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_assets_proto_goTypes,
+		DependencyIndexes: file_assets_proto_depIdxs,
+		MessageInfos:      file_assets_proto_msgTypes,
+	}.Build()
+	File_assets_proto = out.File
+	file_assets_proto_rawDesc = nil
+	file_assets_proto_goTypes = nil
+	file_assets_proto_depIdxs = nil
+}
diff --git a/swapserverrpc/assets.proto b/swapserverrpc/assets.proto
new file mode 100644
index 000000000..7233f100c
--- /dev/null
+++ b/swapserverrpc/assets.proto
@@ -0,0 +1,135 @@
+syntax = "proto3";
+
+// We can't change this to swapserverrpc, it would be a breaking change because
+// the package name is also contained in the HTTP URIs and old clients would
+// call the wrong endpoints. Luckily with the go_package option we can have
+// different golang and RPC package names to fix protobuf namespace conflicts.
+package looprpc;
+
+option go_package = "github.com/lightninglabs/loop/swapserverrpc";
+
+service AssetsSwapServer {
+    // QuoteAssetLoopOut requests a quote for an asset loop out swap from the
+    // server.
+    rpc QuoteAssetLoopOut (QuoteAssetLoopOutRequest)
+        returns (QuoteAssetLoopOutResponse);
+
+    // ListAvailableAssets returns the list of assets that the server supports.
+    rpc ListAvailableAssets (ListAvailableAssetsRequest)
+        returns (ListAvailableAssetsResponse);
+
+    // RequestAssetLoopOut requests an asset loop out swap from the server.
+    rpc RequestAssetLoopOut (RequestAssetLoopOutRequest)
+        returns (RequestAssetLoopOutResponse);
+
+    // PollAssetLoopOutProof requests the server to poll for the proof of the
+    // asset loop out swap.
+    rpc PollAssetLoopOutProof (PollAssetLoopOutProofRequest)
+        returns (PollAssetLoopOutProofResponse);
+
+    // RequestAssetBuy requests an asset buy swap from the server. This
+    // requires an already confirmed asset output on chain.
+    rpc RequestAssetBuy (RequestAssetBuyRequest)
+        returns (RequestAssetBuyResponse);
+
+    // RequestMusig2Sweep requests a musig2 sweep from the server.
+    rpc RequestMusig2Sweep (RequestMusig2SweepRequest)
+        returns (RequestMusig2SweepResponse);
+}
+
+message QuoteAssetLoopOutRequest {
+    // amount is the amount of the asset to loop out.
+    uint64 amount = 1;
+
+    // asset is the asset to loop out.
+    bytes asset = 2;
+}
+
+message QuoteAssetLoopOutResponse {
+    // fixed_prepay_amt is the fixed prepay amount of the swap.
+    uint64 fixed_prepay_amt = 1;
+
+    // swap_fee_rate is the fee rate that is added to the swap invoice.
+    double swap_fee_rate = 2;
+
+    // current_sats_per_asset_unit is the sats per asset unit of the swap.
+    uint64 current_sats_per_asset_unit = 3;
+}
+
+message ListAvailableAssetsRequest {
+}
+
+message ListAvailableAssetsResponse {
+    // assets is the list of assets that the server supports.
+    repeated AssetInfo assets = 1;
+}
+
+message AssetInfo {
+    bytes asset_id = 1;
+    uint64 current_sats_per_asset_unit = 2;
+}
+
+message RequestAssetLoopOutRequest {
+    // amount is the amount of the asset to loop out.
+    uint64 amount = 1;
+
+    // receiver_key is the public key of the receiver.
+    bytes receiver_key = 2;
+
+    // requested_asset is the asset to loop out.
+    bytes requested_asset = 3;
+}
+
+message RequestAssetLoopOutResponse {
+    // swap_hash is the main identifier of the swap.
+    bytes swap_hash = 1;
+
+    // prepay_invoice is the invoice to pay to start the swap. On accepted
+    // the server will publish the htlc output.
+    string prepay_invoice = 2;
+
+    // expiry is the expiry of the htlc output.
+    int64 expiry = 3;
+
+    // sender_pubkey is the public key of the sender.
+    bytes sender_pubkey = 4;
+}
+
+message PollAssetLoopOutProofRequest {
+    // swap_hash is the main identifier of the swap.
+    bytes swap_hash = 1;
+}
+
+message PollAssetLoopOutProofResponse {
+    // raw_proof_file is the raw proof file of the swap.
+    bytes raw_proof_file = 1;
+}
+
+message RequestAssetBuyRequest {
+    // swap_hash is the main identifier of the swap.
+    bytes swap_hash = 1;
+}
+
+message RequestAssetBuyResponse {
+    // swap_invoice is the invoice to pay to receive the preimage to claim the
+    // asset.
+    string swap_invoice = 1;
+}
+
+message RequestMusig2SweepRequest {
+    // swap_hash is the main identifier of the swap.
+    bytes swap_hash = 1;
+    // digest is that the client wants the server to sign.
+    bytes digest = 2;
+    // receiver_nonce is the nonce of the receiver.
+    bytes receiver_nonce = 3;
+    // roothash is the roothash we tweak the musig2 pubkey with.
+    bytes roothash = 4;
+}
+
+message RequestMusig2SweepResponse {
+    // sender_nonce is the nonce of the sender.
+    bytes sender_nonce = 1;
+    // sender_sig is the signature of the sender.
+    bytes sender_sig = 2;
+}
diff --git a/swapserverrpc/assets_grpc.pb.go b/swapserverrpc/assets_grpc.pb.go
new file mode 100644
index 000000000..957bd57f7
--- /dev/null
+++ b/swapserverrpc/assets_grpc.pb.go
@@ -0,0 +1,299 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package swapserverrpc
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// AssetsSwapServerClient is the client API for AssetsSwapServer service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type AssetsSwapServerClient interface {
+	// QuoteAssetLoopOut requests a quote for an asset loop out swap from the
+	// server.
+	QuoteAssetLoopOut(ctx context.Context, in *QuoteAssetLoopOutRequest, opts ...grpc.CallOption) (*QuoteAssetLoopOutResponse, error)
+	// ListAvailableAssets returns the list of assets that the server supports.
+	ListAvailableAssets(ctx context.Context, in *ListAvailableAssetsRequest, opts ...grpc.CallOption) (*ListAvailableAssetsResponse, error)
+	// RequestAssetLoopOut requests an asset loop out swap from the server.
+	RequestAssetLoopOut(ctx context.Context, in *RequestAssetLoopOutRequest, opts ...grpc.CallOption) (*RequestAssetLoopOutResponse, error)
+	// PollAssetLoopOutProof requests the server to poll for the proof of the
+	// asset loop out swap.
+	PollAssetLoopOutProof(ctx context.Context, in *PollAssetLoopOutProofRequest, opts ...grpc.CallOption) (*PollAssetLoopOutProofResponse, error)
+	// RequestAssetBuy requests an asset buy swap from the server. This
+	// requires an already confirmed asset output on chain.
+	RequestAssetBuy(ctx context.Context, in *RequestAssetBuyRequest, opts ...grpc.CallOption) (*RequestAssetBuyResponse, error)
+	// RequestMusig2Sweep requests a musig2 sweep from the server.
+	RequestMusig2Sweep(ctx context.Context, in *RequestMusig2SweepRequest, opts ...grpc.CallOption) (*RequestMusig2SweepResponse, error)
+}
+
+type assetsSwapServerClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewAssetsSwapServerClient(cc grpc.ClientConnInterface) AssetsSwapServerClient {
+	return &assetsSwapServerClient{cc}
+}
+
+func (c *assetsSwapServerClient) QuoteAssetLoopOut(ctx context.Context, in *QuoteAssetLoopOutRequest, opts ...grpc.CallOption) (*QuoteAssetLoopOutResponse, error) {
+	out := new(QuoteAssetLoopOutResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsSwapServer/QuoteAssetLoopOut", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsSwapServerClient) ListAvailableAssets(ctx context.Context, in *ListAvailableAssetsRequest, opts ...grpc.CallOption) (*ListAvailableAssetsResponse, error) {
+	out := new(ListAvailableAssetsResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsSwapServer/ListAvailableAssets", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsSwapServerClient) RequestAssetLoopOut(ctx context.Context, in *RequestAssetLoopOutRequest, opts ...grpc.CallOption) (*RequestAssetLoopOutResponse, error) {
+	out := new(RequestAssetLoopOutResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsSwapServer/RequestAssetLoopOut", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsSwapServerClient) PollAssetLoopOutProof(ctx context.Context, in *PollAssetLoopOutProofRequest, opts ...grpc.CallOption) (*PollAssetLoopOutProofResponse, error) {
+	out := new(PollAssetLoopOutProofResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsSwapServer/PollAssetLoopOutProof", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsSwapServerClient) RequestAssetBuy(ctx context.Context, in *RequestAssetBuyRequest, opts ...grpc.CallOption) (*RequestAssetBuyResponse, error) {
+	out := new(RequestAssetBuyResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsSwapServer/RequestAssetBuy", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *assetsSwapServerClient) RequestMusig2Sweep(ctx context.Context, in *RequestMusig2SweepRequest, opts ...grpc.CallOption) (*RequestMusig2SweepResponse, error) {
+	out := new(RequestMusig2SweepResponse)
+	err := c.cc.Invoke(ctx, "/looprpc.AssetsSwapServer/RequestMusig2Sweep", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// AssetsSwapServerServer is the server API for AssetsSwapServer service.
+// All implementations must embed UnimplementedAssetsSwapServerServer
+// for forward compatibility
+type AssetsSwapServerServer interface {
+	// QuoteAssetLoopOut requests a quote for an asset loop out swap from the
+	// server.
+	QuoteAssetLoopOut(context.Context, *QuoteAssetLoopOutRequest) (*QuoteAssetLoopOutResponse, error)
+	// ListAvailableAssets returns the list of assets that the server supports.
+	ListAvailableAssets(context.Context, *ListAvailableAssetsRequest) (*ListAvailableAssetsResponse, error)
+	// RequestAssetLoopOut requests an asset loop out swap from the server.
+	RequestAssetLoopOut(context.Context, *RequestAssetLoopOutRequest) (*RequestAssetLoopOutResponse, error)
+	// PollAssetLoopOutProof requests the server to poll for the proof of the
+	// asset loop out swap.
+	PollAssetLoopOutProof(context.Context, *PollAssetLoopOutProofRequest) (*PollAssetLoopOutProofResponse, error)
+	// RequestAssetBuy requests an asset buy swap from the server. This
+	// requires an already confirmed asset output on chain.
+	RequestAssetBuy(context.Context, *RequestAssetBuyRequest) (*RequestAssetBuyResponse, error)
+	// RequestMusig2Sweep requests a musig2 sweep from the server.
+	RequestMusig2Sweep(context.Context, *RequestMusig2SweepRequest) (*RequestMusig2SweepResponse, error)
+	mustEmbedUnimplementedAssetsSwapServerServer()
+}
+
+// UnimplementedAssetsSwapServerServer must be embedded to have forward compatible implementations.
+type UnimplementedAssetsSwapServerServer struct {
+}
+
+func (UnimplementedAssetsSwapServerServer) QuoteAssetLoopOut(context.Context, *QuoteAssetLoopOutRequest) (*QuoteAssetLoopOutResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method QuoteAssetLoopOut not implemented")
+}
+func (UnimplementedAssetsSwapServerServer) ListAvailableAssets(context.Context, *ListAvailableAssetsRequest) (*ListAvailableAssetsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ListAvailableAssets not implemented")
+}
+func (UnimplementedAssetsSwapServerServer) RequestAssetLoopOut(context.Context, *RequestAssetLoopOutRequest) (*RequestAssetLoopOutResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method RequestAssetLoopOut not implemented")
+}
+func (UnimplementedAssetsSwapServerServer) PollAssetLoopOutProof(context.Context, *PollAssetLoopOutProofRequest) (*PollAssetLoopOutProofResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method PollAssetLoopOutProof not implemented")
+}
+func (UnimplementedAssetsSwapServerServer) RequestAssetBuy(context.Context, *RequestAssetBuyRequest) (*RequestAssetBuyResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method RequestAssetBuy not implemented")
+}
+func (UnimplementedAssetsSwapServerServer) RequestMusig2Sweep(context.Context, *RequestMusig2SweepRequest) (*RequestMusig2SweepResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method RequestMusig2Sweep not implemented")
+}
+func (UnimplementedAssetsSwapServerServer) mustEmbedUnimplementedAssetsSwapServerServer() {}
+
+// UnsafeAssetsSwapServerServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to AssetsSwapServerServer will
+// result in compilation errors.
+type UnsafeAssetsSwapServerServer interface {
+	mustEmbedUnimplementedAssetsSwapServerServer()
+}
+
+func RegisterAssetsSwapServerServer(s grpc.ServiceRegistrar, srv AssetsSwapServerServer) {
+	s.RegisterService(&AssetsSwapServer_ServiceDesc, srv)
+}
+
+func _AssetsSwapServer_QuoteAssetLoopOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(QuoteAssetLoopOutRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsSwapServerServer).QuoteAssetLoopOut(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsSwapServer/QuoteAssetLoopOut",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsSwapServerServer).QuoteAssetLoopOut(ctx, req.(*QuoteAssetLoopOutRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsSwapServer_ListAvailableAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListAvailableAssetsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsSwapServerServer).ListAvailableAssets(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsSwapServer/ListAvailableAssets",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsSwapServerServer).ListAvailableAssets(ctx, req.(*ListAvailableAssetsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsSwapServer_RequestAssetLoopOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RequestAssetLoopOutRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsSwapServerServer).RequestAssetLoopOut(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsSwapServer/RequestAssetLoopOut",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsSwapServerServer).RequestAssetLoopOut(ctx, req.(*RequestAssetLoopOutRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsSwapServer_PollAssetLoopOutProof_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(PollAssetLoopOutProofRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsSwapServerServer).PollAssetLoopOutProof(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsSwapServer/PollAssetLoopOutProof",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsSwapServerServer).PollAssetLoopOutProof(ctx, req.(*PollAssetLoopOutProofRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsSwapServer_RequestAssetBuy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RequestAssetBuyRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsSwapServerServer).RequestAssetBuy(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsSwapServer/RequestAssetBuy",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsSwapServerServer).RequestAssetBuy(ctx, req.(*RequestAssetBuyRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _AssetsSwapServer_RequestMusig2Sweep_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RequestMusig2SweepRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(AssetsSwapServerServer).RequestMusig2Sweep(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/looprpc.AssetsSwapServer/RequestMusig2Sweep",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(AssetsSwapServerServer).RequestMusig2Sweep(ctx, req.(*RequestMusig2SweepRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// AssetsSwapServer_ServiceDesc is the grpc.ServiceDesc for AssetsSwapServer service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var AssetsSwapServer_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "looprpc.AssetsSwapServer",
+	HandlerType: (*AssetsSwapServerServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "QuoteAssetLoopOut",
+			Handler:    _AssetsSwapServer_QuoteAssetLoopOut_Handler,
+		},
+		{
+			MethodName: "ListAvailableAssets",
+			Handler:    _AssetsSwapServer_ListAvailableAssets_Handler,
+		},
+		{
+			MethodName: "RequestAssetLoopOut",
+			Handler:    _AssetsSwapServer_RequestAssetLoopOut_Handler,
+		},
+		{
+			MethodName: "PollAssetLoopOutProof",
+			Handler:    _AssetsSwapServer_PollAssetLoopOutProof_Handler,
+		},
+		{
+			MethodName: "RequestAssetBuy",
+			Handler:    _AssetsSwapServer_RequestAssetBuy_Handler,
+		},
+		{
+			MethodName: "RequestMusig2Sweep",
+			Handler:    _AssetsSwapServer_RequestMusig2Sweep_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "assets.proto",
+}
diff --git a/utils/listeners.go b/utils/listeners.go
new file mode 100644
index 000000000..54716d8d3
--- /dev/null
+++ b/utils/listeners.go
@@ -0,0 +1,257 @@
+package utils
+
+import (
+	"context"
+	"sync"
+
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/lightninglabs/lndclient"
+	"github.com/lightningnetwork/lnd/chainntnfs"
+	"github.com/lightningnetwork/lnd/lntypes"
+)
+
+// ExpiryManager is a manager for block height expiry events.
+type ExpiryManager struct {
+	chainNotifier lndclient.ChainNotifierClient
+
+	expiryHeightMap map[[32]byte]int32
+	expiryFuncMap   map[[32]byte]func()
+
+	currentBlockHeight int32
+
+	sync.Mutex
+}
+
+// NewExpiryManager creates a new expiry manager.
+func NewExpiryManager(
+	chainNotifier lndclient.ChainNotifierClient) *ExpiryManager {
+
+	return &ExpiryManager{
+		chainNotifier:   chainNotifier,
+		expiryHeightMap: make(map[[32]byte]int32),
+		expiryFuncMap:   make(map[[32]byte]func()),
+	}
+}
+
+// Start starts the expiry manager and listens for block height notifications.
+func (e *ExpiryManager) Start(ctx context.Context, startingBlockHeight int32,
+) error {
+
+	e.Lock()
+	e.currentBlockHeight = startingBlockHeight
+	e.Unlock()
+
+	log.Debugf("Starting expiry manager at height %d", startingBlockHeight)
+	defer log.Debugf("Expiry manager stopped")
+
+	blockHeightChan, errChan, err := e.chainNotifier.RegisterBlockEpochNtfn(
+		ctx,
+	)
+	if err != nil {
+		return err
+	}
+
+	for {
+		select {
+		case blockHeight := <-blockHeightChan:
+
+			log.Debugf("Received block height %d", blockHeight)
+
+			e.Lock()
+			e.currentBlockHeight = blockHeight
+			e.Unlock()
+
+			e.checkExpiry(blockHeight)
+
+		case err := <-errChan:
+			log.Debugf("Expiry manager error")
+			return err
+
+		case <-ctx.Done():
+			log.Debugf("Expiry manager stopped")
+			return nil
+		}
+	}
+}
+
+// GetBlockHeight returns the current block height.
+func (e *ExpiryManager) GetBlockHeight() int32 {
+	e.Lock()
+	defer e.Unlock()
+
+	return e.currentBlockHeight
+}
+
+// checkExpiry checks if any swaps have expired and calls the expiry function if
+// they have.
+func (e *ExpiryManager) checkExpiry(blockHeight int32) {
+	e.Lock()
+	defer e.Unlock()
+
+	for swapHash, expiryHeight := range e.expiryHeightMap {
+		if blockHeight >= expiryHeight {
+			expiryFunc := e.expiryFuncMap[swapHash]
+			go expiryFunc()
+
+			delete(e.expiryHeightMap, swapHash)
+			delete(e.expiryFuncMap, swapHash)
+		}
+	}
+}
+
+// SubscribeExpiry subscribes to an expiry event for a swap. If the expiry height
+// has already been reached, the expiryFunc is not called and the function
+// returns true. Otherwise, the expiryFunc is called when the expiry height is
+// reached and the function returns false.
+func (e *ExpiryManager) SubscribeExpiry(swapHash [32]byte,
+	expiryHeight int32, expiryFunc func()) bool {
+
+	e.Lock()
+	defer e.Unlock()
+
+	if e.currentBlockHeight >= expiryHeight {
+		return true
+	}
+
+	log.Debugf("Subscribing to expiry for swap %x at height %d",
+		swapHash, expiryHeight)
+
+	e.expiryHeightMap[swapHash] = expiryHeight
+	e.expiryFuncMap[swapHash] = expiryFunc
+
+	return false
+}
+
+// SubscribeInvoiceManager is a manager for invoice subscription events.
+type SubscribeInvoiceManager struct {
+	invoicesClient lndclient.InvoicesClient
+
+	subscribers map[[32]byte]struct{}
+
+	sync.Mutex
+}
+
+// NewSubscribeInvoiceManager creates a new subscribe invoice manager.
+func NewSubscribeInvoiceManager(
+	invoicesClient lndclient.InvoicesClient) *SubscribeInvoiceManager {
+
+	return &SubscribeInvoiceManager{
+		invoicesClient: invoicesClient,
+		subscribers:    make(map[[32]byte]struct{}),
+	}
+}
+
+// SubscribeInvoice subscribes to invoice events for a swap hash. The update
+// callback is called when the invoice is updated and the error callback is
+// called when an error occurs.
+func (s *SubscribeInvoiceManager) SubscribeInvoice(ctx context.Context,
+	invoiceHash lntypes.Hash, callback func(lndclient.InvoiceUpdate, error),
+) error {
+
+	s.Lock()
+	defer s.Unlock()
+	// If we already have a subscriber for this swap hash, return early.
+	if _, ok := s.subscribers[invoiceHash]; ok {
+		return nil
+	}
+
+	log.Debugf("Subscribing to invoice %v", invoiceHash)
+
+	updateChan, errChan, err := s.invoicesClient.SubscribeSingleInvoice(
+		ctx, invoiceHash,
+	)
+	if err != nil {
+		return err
+	}
+
+	s.subscribers[invoiceHash] = struct{}{}
+
+	go func() {
+		for {
+			select {
+			case update := <-updateChan:
+				callback(update, nil)
+
+			case err := <-errChan:
+				callback(lndclient.InvoiceUpdate{}, err)
+				delete(s.subscribers, invoiceHash)
+				return
+
+			case <-ctx.Done():
+				delete(s.subscribers, invoiceHash)
+				return
+			}
+		}
+	}()
+
+	return nil
+}
+
+// TxSubscribeConfirmationManager is a manager for transaction confirmation
+// subscription events.
+type TxSubscribeConfirmationManager struct {
+	chainNotifier lndclient.ChainNotifierClient
+
+	subscribers map[[32]byte]struct{}
+
+	sync.Mutex
+}
+
+// NewTxSubscribeConfirmationManager creates a new transaction confirmation
+// subscription manager.
+func NewTxSubscribeConfirmationManager(chainNtfn lndclient.ChainNotifierClient,
+) *TxSubscribeConfirmationManager {
+
+	return &TxSubscribeConfirmationManager{
+		chainNotifier: chainNtfn,
+		subscribers:   make(map[[32]byte]struct{}),
+	}
+}
+
+// SubscribeTxConfirmation subscribes to transaction confirmation events for a
+// swap hash. The callback is called when the transaction is confirmed or an
+// error occurs.
+func (t *TxSubscribeConfirmationManager) SubscribeTxConfirmation(
+	ctx context.Context, swapHash lntypes.Hash, txid *chainhash.Hash,
+	pkscript []byte, numConfs int32, heightHint int32,
+	cb func(*chainntnfs.TxConfirmation, error)) error {
+
+	t.Lock()
+	defer t.Unlock()
+
+	// If we already have a subscriber for this swap hash, return early.
+	if _, ok := t.subscribers[swapHash]; ok {
+		return nil
+	}
+
+	log.Debugf("Subscribing to tx confirmation for swap %v", swapHash)
+
+	confChan, errChan, err := t.chainNotifier.RegisterConfirmationsNtfn(
+		ctx, txid, pkscript, numConfs, heightHint,
+	)
+	if err != nil {
+		return err
+	}
+
+	t.subscribers[swapHash] = struct{}{}
+
+	go func() {
+		for {
+			select {
+			case conf := <-confChan:
+				cb(conf, nil)
+
+			case err := <-errChan:
+				cb(nil, err)
+				delete(t.subscribers, swapHash)
+				return
+
+			case <-ctx.Done():
+				delete(t.subscribers, swapHash)
+				return
+			}
+		}
+	}()
+
+	return nil
+}
diff --git a/utils/log.go b/utils/log.go
new file mode 100644
index 000000000..fcefd75c4
--- /dev/null
+++ b/utils/log.go
@@ -0,0 +1,26 @@
+package utils
+
+import (
+	"github.com/btcsuite/btclog/v2"
+	"github.com/lightningnetwork/lnd/build"
+)
+
+// Subsystem defines the sub system name of this package.
+const Subsystem = "UTILS"
+
+// log is a logger that is initialized with no output filters.  This
+// means the package will not perform any logging by default until the caller
+// requests it.
+var log btclog.Logger
+
+// The default amount of logging is none.
+func init() {
+	UseLogger(build.NewSubLogger(Subsystem, nil))
+}
+
+// UseLogger uses a specified Logger to output package logging info.
+// This should be used in preference to SetLogWriter if the caller is also
+// using btclog.
+func UseLogger(logger btclog.Logger) {
+	log = logger
+}