diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -4367,19 +4367,26 @@
     'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
     'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
     'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
+    'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php',
     'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php',
+    'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php',
     'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php',
     'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php',
     'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
     'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
     'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php',
+    'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php',
+    'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php',
     'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php',
+    'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php',
     'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
     'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php',
+    'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php',
     'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
     'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
     'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
     'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
+    'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php',
     'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php',
     'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
     'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
@@ -9820,19 +9827,26 @@
       'PhabricatorPolicyInterface',
     ),
     'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
+    'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType',
     'PhortuneMerchantController' => 'PhortuneController',
+    'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType',
     'PhortuneMerchantEditController' => 'PhortuneMerchantController',
     'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine',
     'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
     'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
     'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController',
+    'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType',
+    'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType',
     'PhortuneMerchantListController' => 'PhortuneMerchantController',
+    'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType',
     'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
     'PhortuneMerchantPictureController' => 'PhortuneMerchantController',
+    'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType',
     'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'PhortuneMerchantTransaction' => 'PhabricatorApplicationTransaction',
+    'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction',
     'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType',
     'PhortuneMerchantViewController' => 'PhortuneMerchantController',
     'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
     'PhortuneOrderTableView' => 'AphrontView',
diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php
--- a/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php
+++ b/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php
@@ -76,7 +76,8 @@
 
         $xactions = array();
         $xactions[] = id(new PhortuneMerchantTransaction())
-          ->setTransactionType(PhortuneMerchantTransaction::TYPE_PICTURE)
+          ->setTransactionType(
+            PhortuneMerchantPictureTransaction::TRANSACTIONTYPE)
           ->setNewValue($new_value);
 
         $editor = id(new PhortuneMerchantEditor())
diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php
--- a/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php
+++ b/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php
@@ -240,7 +240,7 @@
     }
 
     $curtain->newPanel()
-      ->setHeaderText(pht('Members'))
+      ->setHeaderText(pht('Managers'))
       ->appendChild($member_list);
 
     return $curtain;
diff --git a/src/applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php b/src/applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php
--- a/src/applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php
+++ b/src/applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php
@@ -14,7 +14,7 @@
     $add_edges) {
 
     return pht(
-      '%s added %s merchant member(s): %s.',
+      '%s added %s merchant manager(s): %s.',
       $actor,
       $add_count,
       $add_edges);
@@ -26,7 +26,7 @@
     $rem_edges) {
 
     return pht(
-      '%s removed %s merchant member(s): %s.',
+      '%s removed %s merchant manager(s): %s.',
       $actor,
       $rem_count,
       $rem_edges);
@@ -41,7 +41,7 @@
     $rem_edges) {
 
     return pht(
-      '%s edited %s merchant member(s), added %s: %s; removed %s: %s.',
+      '%s edited %s merchant manager(s), added %s: %s; removed %s: %s.',
       $actor,
       $total_count,
       $add_count,
@@ -57,7 +57,7 @@
     $add_edges) {
 
     return pht(
-      '%s added %s merchant member(s) to %s: %s.',
+      '%s added %s merchant manager(s) to %s: %s.',
       $actor,
       $add_count,
       $object,
@@ -71,7 +71,7 @@
     $rem_edges) {
 
     return pht(
-      '%s removed %s merchant member(s) from %s: %s.',
+      '%s removed %s merchant manager(s) from %s: %s.',
       $actor,
       $rem_count,
       $object,
@@ -88,7 +88,7 @@
     $rem_edges) {
 
     return pht(
-      '%s edited %s merchant member(s) for %s, added %s: %s; removed %s: %s.',
+      '%s edited %s merchant manager(s) for %s, added %s: %s; removed %s: %s.',
       $actor,
       $total_count,
       $object,
diff --git a/src/applications/phortune/editor/PhortuneMerchantEditEngine.php b/src/applications/phortune/editor/PhortuneMerchantEditEngine.php
--- a/src/applications/phortune/editor/PhortuneMerchantEditEngine.php
+++ b/src/applications/phortune/editor/PhortuneMerchantEditEngine.php
@@ -81,21 +81,22 @@
         ->setDescription(pht('Merchant name.'))
         ->setConduitTypeDescription(pht('New Merchant name.'))
         ->setIsRequired(true)
-        ->setTransactionType(PhortuneMerchantTransaction::TYPE_NAME)
+        ->setTransactionType(
+          PhortuneMerchantNameTransaction::TRANSACTIONTYPE)
         ->setValue($object->getName()),
 
       id(new PhabricatorUsersEditField())
         ->setKey('members')
-        ->setAliases(array('memberPHIDs'))
-        ->setLabel(pht('Members'))
+        ->setAliases(array('memberPHIDs', 'managerPHIDs'))
+        ->setLabel(pht('Managers'))
         ->setUseEdgeTransactions(true)
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue(
           'edge:type',
           PhortuneMerchantHasMemberEdgeType::EDGECONST)
-        ->setDescription(pht('Initial merchant members.'))
-        ->setConduitDescription(pht('Set merchant members.'))
-        ->setConduitTypeDescription(pht('New list of members.'))
+        ->setDescription(pht('Initial merchant managers.'))
+        ->setConduitDescription(pht('Set merchant managers.'))
+        ->setConduitTypeDescription(pht('New list of managers.'))
         ->setInitialValue($object->getMemberPHIDs())
         ->setValue($member_phids),
 
@@ -104,7 +105,8 @@
         ->setLabel(pht('Description'))
         ->setDescription(pht('Merchant description.'))
         ->setConduitTypeDescription(pht('New merchant description.'))
-        ->setTransactionType(PhortuneMerchantTransaction::TYPE_DESCRIPTION)
+        ->setTransactionType(
+          PhortuneMerchantDescriptionTransaction::TRANSACTIONTYPE)
         ->setValue($object->getDescription()),
 
       id(new PhabricatorRemarkupEditField())
@@ -112,7 +114,8 @@
         ->setLabel(pht('Contact Info'))
         ->setDescription(pht('Merchant contact information.'))
         ->setConduitTypeDescription(pht('Merchant contact information.'))
-        ->setTransactionType(PhortuneMerchantTransaction::TYPE_CONTACTINFO)
+        ->setTransactionType(
+          PhortuneMerchantContactInfoTransaction::TRANSACTIONTYPE)
         ->setValue($object->getContactInfo()),
 
       id(new PhabricatorTextEditField())
@@ -121,7 +124,8 @@
         ->setDescription(pht('Email address invoices are sent from.'))
         ->setConduitTypeDescription(
           pht('Email address invoices are sent from.'))
-        ->setTransactionType(PhortuneMerchantTransaction::TYPE_INVOICEEMAIL)
+        ->setTransactionType(
+          PhortuneMerchantInvoiceEmailTransaction::TRANSACTIONTYPE)
         ->setValue($object->getInvoiceEmail()),
 
       id(new PhabricatorRemarkupEditField())
@@ -129,7 +133,8 @@
         ->setLabel(pht('Invoice Footer'))
         ->setDescription(pht('Footer on invoice forms.'))
         ->setConduitTypeDescription(pht('Footer on invoice forms.'))
-        ->setTransactionType(PhortuneMerchantTransaction::TYPE_INVOICEFOOTER)
+        ->setTransactionType(
+          PhortuneMerchantInvoiceFooterTransaction::TRANSACTIONTYPE)
         ->setValue($object->getInvoiceFooter()),
 
     );
diff --git a/src/applications/phortune/editor/PhortuneMerchantEditor.php b/src/applications/phortune/editor/PhortuneMerchantEditor.php
--- a/src/applications/phortune/editor/PhortuneMerchantEditor.php
+++ b/src/applications/phortune/editor/PhortuneMerchantEditor.php
@@ -11,104 +11,19 @@
     return pht('Phortune Merchants');
   }
 
+  public function getCreateObjectTitle($author, $object) {
+    return pht('%s created this merchant.', $author);
+  }
+
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
-    $types[] = PhortuneMerchantTransaction::TYPE_NAME;
-    $types[] = PhortuneMerchantTransaction::TYPE_DESCRIPTION;
-    $types[] = PhortuneMerchantTransaction::TYPE_CONTACTINFO;
-    $types[] = PhortuneMerchantTransaction::TYPE_PICTURE;
-    $types[] = PhortuneMerchantTransaction::TYPE_INVOICEEMAIL;
-    $types[] = PhortuneMerchantTransaction::TYPE_INVOICEFOOTER;
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDGE;
 
     return $types;
   }
 
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-    switch ($xaction->getTransactionType()) {
-      case PhortuneMerchantTransaction::TYPE_NAME:
-        return $object->getName();
-      case PhortuneMerchantTransaction::TYPE_DESCRIPTION:
-        return $object->getDescription();
-      case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
-        return $object->getContactInfo();
-      case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
-        return $object->getInvoiceEmail();
-      case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
-        return $object->getInvoiceFooter();
-      case PhortuneMerchantTransaction::TYPE_PICTURE:
-        return $object->getProfileImagePHID();
-    }
-
-    return parent::getCustomTransactionOldValue($object, $xaction);
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhortuneMerchantTransaction::TYPE_NAME:
-      case PhortuneMerchantTransaction::TYPE_DESCRIPTION:
-      case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
-      case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
-      case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
-      case PhortuneMerchantTransaction::TYPE_PICTURE:
-        return $xaction->getNewValue();
-    }
-
-    return parent::getCustomTransactionNewValue($object, $xaction);
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhortuneMerchantTransaction::TYPE_NAME:
-        $object->setName($xaction->getNewValue());
-        return;
-      case PhortuneMerchantTransaction::TYPE_DESCRIPTION:
-        $object->setDescription($xaction->getNewValue());
-        return;
-      case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
-        $object->setContactInfo($xaction->getNewValue());
-        return;
-      case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
-        $object->setInvoiceEmail($xaction->getNewValue());
-        return;
-      case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
-        $object->setInvoiceFooter($xaction->getNewValue());
-        return;
-      case PhortuneMerchantTransaction::TYPE_PICTURE:
-        $object->setProfileImagePHID($xaction->getNewValue());
-        return;
-    }
-
-    return parent::applyCustomInternalTransaction($object, $xaction);
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhortuneMerchantTransaction::TYPE_NAME:
-      case PhortuneMerchantTransaction::TYPE_DESCRIPTION:
-      case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
-      case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
-      case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
-      case PhortuneMerchantTransaction::TYPE_PICTURE:
-        return;
-    }
-
-    return parent::applyCustomExternalTransaction($object, $xaction);
-  }
-
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
@@ -117,48 +32,28 @@
     $errors = parent::validateTransaction($object, $type, $xactions);
 
     switch ($type) {
-      case PhortuneMerchantTransaction::TYPE_NAME:
-        $missing = $this->validateIsEmptyTextField(
-          $object->getName(),
-          $xactions);
-
-        if ($missing) {
-          $error = new PhabricatorApplicationTransactionValidationError(
-            $type,
-            pht('Required'),
-            pht('Merchant name is required.'),
-            nonempty(last($xactions), null));
-
-          $error->setIsMissingFieldError(true);
-          $errors[] = $error;
-        }
-       break;
-      case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
-        $new_email = null;
+      case PhabricatorTransactions::TYPE_EDGE:
         foreach ($xactions as $xaction) {
-          switch ($xaction->getTransactionType()) {
-            case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
-              $new_email = $xaction->getNewValue();
-              break;
-          }
-        }
-        if (strlen($new_email)) {
-          $email = new PhutilEmailAddress($new_email);
-          $domain = $email->getDomainName();
-
-          if (!$domain) {
-            $error = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              pht('%s is not a valid email.', $new_email),
-              nonempty(last($xactions), null));
-
-            $errors[] = $error;
+          switch ($xaction->getMetadataValue('edge:type')) {
+            case PhortuneMerchantHasMemberEdgeType::EDGECONST:
+              $new = $xaction->getNewValue();
+              $set = idx($new, '-', array());
+              $actor_phid = $this->requireActor()->getPHID();
+              foreach ($set as $phid) {
+                if ($actor_phid == $phid) {
+                  $error = new PhabricatorApplicationTransactionValidationError(
+                    $type,
+                    pht('Invalid'),
+                    pht('You can not remove yourself as an merchant manager.'),
+                    $xaction);
+                  $errors[] = $error;
+                }
+              }
+            break;
           }
         }
         break;
     }
-
     return $errors;
   }
 
diff --git a/src/applications/phortune/storage/PhortuneMerchantTransaction.php b/src/applications/phortune/storage/PhortuneMerchantTransaction.php
--- a/src/applications/phortune/storage/PhortuneMerchantTransaction.php
+++ b/src/applications/phortune/storage/PhortuneMerchantTransaction.php
@@ -1,14 +1,7 @@
 <?php
 
 final class PhortuneMerchantTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_NAME = 'merchant:name';
-  const TYPE_DESCRIPTION = 'merchant:description';
-  const TYPE_CONTACTINFO = 'merchant:contactinfo';
-  const TYPE_INVOICEEMAIL = 'merchant:invoiceemail';
-  const TYPE_INVOICEFOOTER = 'merchant:invoicefooter';
-  const TYPE_PICTURE = 'merchant:picture';
+  extends PhabricatorModularTransaction {
 
   public function getApplicationName() {
     return 'phortune';
@@ -22,79 +15,8 @@
     return null;
   }
 
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        if ($old === null) {
-          return pht(
-            '%s created this merchant.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s renamed this merchant from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-        break;
-      case self::TYPE_DESCRIPTION:
-        return pht(
-          '%s updated the description for this merchant.',
-            $this->renderHandleLink($author_phid));
-      case self::TYPE_CONTACTINFO:
-        return pht(
-          '%s updated the contact information for this merchant.',
-            $this->renderHandleLink($author_phid));
-      case self::TYPE_INVOICEEMAIL:
-        return pht(
-          '%s updated the invoice email for this merchant.',
-            $this->renderHandleLink($author_phid));
-      case self::TYPE_INVOICEFOOTER:
-        return pht(
-          '%s updated the invoice footer for this merchant.',
-            $this->renderHandleLink($author_phid));
-    }
-
-    return parent::getTitle();
-  }
-
-  public function shouldHide() {
-    $old = $this->getOldValue();
-    switch ($this->getTransactionType()) {
-      case self::TYPE_DESCRIPTION:
-      case self::TYPE_CONTACTINFO:
-      case self::TYPE_INVOICEEMAIL:
-      case self::TYPE_INVOICEFOOTER:
-        return ($old === null);
-    }
-    return parent::shouldHide();
-  }
-
-  public function hasChangeDetails() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_DESCRIPTION:
-        return ($this->getOldValue() !== null);
-      case self::TYPE_CONTACTINFO:
-        return ($this->getOldValue() !== null);
-      case self::TYPE_INVOICEEMAIL:
-        return ($this->getOldValue() !== null);
-      case self::TYPE_INVOICEFOOTER:
-        return ($this->getOldValue() !== null);
-    }
-
-    return parent::hasChangeDetails();
-  }
-
-  public function renderChangeDetails(PhabricatorUser $viewer) {
-    return $this->renderTextCorpusChangeDetails(
-      $viewer,
-      $this->getOldValue(),
-      $this->getNewValue());
+  public function getBaseTransactionClass() {
+    return 'PhortuneMerchantTransactionType';
   }
 
 }
diff --git a/src/applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php b/src/applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhortuneMerchantContactInfoTransaction
+  extends PhortuneMerchantTransactionType {
+
+  const TRANSACTIONTYPE = 'merchant:contactinfo';
+
+  public function generateOldValue($object) {
+    return $object->getContactInfo();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setContactInfo($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the merchant contact info.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the merchant contact info for %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO MERCHANT CONTACT INFO');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php b/src/applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhortuneMerchantDescriptionTransaction
+  extends PhortuneMerchantTransactionType {
+
+  const TRANSACTIONTYPE = 'merchant:description';
+
+  public function generateOldValue($object) {
+    return $object->getDescription();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDescription($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the merchant description.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the merchant description for %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO MERCHANT DESCRIPTION');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php b/src/applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php
@@ -0,0 +1,94 @@
+<?php
+
+final class PhortuneMerchantInvoiceEmailTransaction
+  extends PhortuneMerchantTransactionType {
+
+  const TRANSACTIONTYPE = 'merchant:invoiceemail';
+
+  public function generateOldValue($object) {
+    return $object->getInvoiceEmail();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setInvoiceEmail($value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (strlen($old) && strlen($new)) {
+      return pht(
+        '%s updated the invoice email from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    } else if (strlen($old)) {
+      return pht(
+        '%s removed the invoice email.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+      '%s set the invoice email to %s.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function getTitleForFeed() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (strlen($old) && strlen($new)) {
+      return pht(
+        '%s updated %s invoice email from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderObject(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    } else if (strlen($old)) {
+      return pht(
+        '%s removed the invoice email for %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    } else {
+      return pht(
+      '%s set the invoice email for %s to %s.',
+        $this->renderAuthor(),
+        $this->renderObject(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function getIcon() {
+    return 'fa-envelope';
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $max_length = $object->getColumnMaximumByteLength('invoiceEmail');
+    foreach ($xactions as $xaction) {
+      if (strlen($xaction->getNewValue())) {
+        $email = new PhutilEmailAddress($xaction->getNewValue());
+        $domain = $email->getDomainName();
+        if (!strlen($domain)) {
+          $errors[] = $this->newInvalidError(
+            pht('Invoice email "%s" must be a valid email.',
+            $xaction->getNewValue()));
+        }
+
+        $new_value = $xaction->getNewValue();
+        $new_length = strlen($new_value);
+        if ($new_length > $max_length) {
+          $errors[] = $this->newInvalidError(
+            pht('The email can be no longer than %s characters.',
+            new PhutilNumber($max_length)));
+        }
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php b/src/applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhortuneMerchantInvoiceFooterTransaction
+  extends PhortuneMerchantTransactionType {
+
+  const TRANSACTIONTYPE = 'merchant:invoicefooter';
+
+  public function generateOldValue($object) {
+    return $object->getInvoiceFooter();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setInvoiceFooter($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the merchant invoice footer.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the merchant invoice footer for %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO MERCHANT INVOICE FOOTER');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneMerchantNameTransaction.php b/src/applications/phortune/xaction/PhortuneMerchantNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantNameTransaction.php
@@ -0,0 +1,55 @@
+<?php
+
+final class PhortuneMerchantNameTransaction
+  extends PhortuneMerchantTransactionType {
+
+  const TRANSACTIONTYPE = 'merchant:name';
+
+  public function generateOldValue($object) {
+    return $object->getName();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setName($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s renamed this merchant from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s renamed %s merchant name from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderObject(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Merchants must have a name.'));
+    }
+
+    $max_length = $object->getColumnMaximumByteLength('name');
+    foreach ($xactions as $xaction) {
+      $new_value = $xaction->getNewValue();
+      $new_length = strlen($new_value);
+      if ($new_length > $max_length) {
+        $errors[] = $this->newInvalidError(
+          pht('The name can be no longer than %s characters.',
+          new PhutilNumber($max_length)));
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneMerchantPictureTransaction.php b/src/applications/phortune/xaction/PhortuneMerchantPictureTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantPictureTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhortuneMerchantPictureTransaction
+  extends PhortuneMerchantTransactionType {
+
+  const TRANSACTIONTYPE = 'merchant:picture';
+
+  public function generateOldValue($object) {
+    return $object->getProfileImagePHID();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setProfileImagePHID($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the picture.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s updated the picture for merchant %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function getIcon() {
+    return 'fa-camera-retro';
+  }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneMerchantTransactionType.php b/src/applications/phortune/xaction/PhortuneMerchantTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneMerchantTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhortuneMerchantTransactionType
+  extends PhabricatorModularTransactionType {}
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -1610,6 +1610,20 @@
 
       '%s accepted this revision as %s reviewer(s): %s.' =>
         '%s accepted this revision as: %3$s.',
+
+      '%s added %s merchant manager(s): %s.' => array(
+        array(
+          '%s added a merchant manager: %3$s.',
+          '%s added merchant managers: %3$s.',
+        ),
+      ),
+
+      '%s removed %s merchant manager(s): %s.' => array(
+        array(
+          '%s removed a merchant manager: %3$s.',
+          '%s removed merchant managers: %3$s.',
+        ),
+      ),
     );
   }