diff --git a/chat.py b/chat.py
index e6dafba..ec09019 100644
--- a/chat.py
+++ b/chat.py
@@ -4,6 +4,7 @@ import asyncio
 import enum
 import json
 import logging
+import time
 from typing import *
 
 import aiohttp
@@ -65,19 +66,38 @@ class Room(blivedm.BLiveClient):
             client.write_message(body)
 
     async def __my_on_get_danmaku(self, command):
-        data = {
+        self.send_message(Command.ADD_TEXT, {
             'avatarUrl': await get_avatar_url(command['info'][2][0]),
             'timestamp': command['info'][0][4],
-            'content': command['info'][1],
-            'authorName': command['info'][2][1]
-        }
-        self.send_message(Command.ADD_TEXT, data)
+            'authorName': command['info'][2][1],
+            'content': command['info'][1]
+        })
 
     _COMMAND_HANDLERS['DANMU_MSG'] = __my_on_get_danmaku
 
-    # 新舰长 {'cmd': 'GUARD_BUY', 'data': {'uid': 1822222, 'username': 'MRSKING', 'guard_level': 3,
-    # 'num': 1, 'price': 198000, 'gift_id': 10003, 'gift_name': '舰长', 'start_time': 1558506165,
-    # 'end_time': 1558506165}}
+    async def __my_on_gift(self, command):
+        if command['data']['coin_type'] != 'gold':  # 丢人
+            return
+        self.send_message(Command.ADD_GIFT, {
+            'avatarUrl': await get_avatar_url(command['data']['uid']),
+            'authorName': command['data']['uname'],
+            'giftName': command['data']['giftName'],
+            'giftNum': command['data']['num'],
+            'totalCoin': command['data']['total_coin']
+        })
+
+    _COMMAND_HANDLERS['SEND_GIFT'] = __my_on_gift
+
+    async def __on_new_member(self, command):
+        # 新舰长 {'cmd': 'GUARD_BUY', 'data': {'uid': 1822222, 'username': 'MRSKING', 'guard_level': 3,
+        # 'num': 1, 'price': 198000, 'gift_id': 10003, 'gift_name': '舰长', 'start_time': 1558506165,
+        # 'end_time': 1558506165}}
+        self.send_message(Command.ADD_VIP, {
+            'avatarUrl':  await get_avatar_url(command['data']['uid']),
+            'authorName': command['data']['username'],
+        })
+
+    _COMMAND_HANDLERS['GUARD_BUY'] = __on_new_member
 
 
 class RoomManager:
@@ -94,6 +114,9 @@ class RoomManager:
             room.start()
         room.clients.append(client)
 
+        # 测试用
+        # self.__send_test_message(room)
+
     def del_client(self, room_id, client: 'ChatHandler'):
         if room_id not in self._rooms:
             return
@@ -104,6 +127,54 @@ class RoomManager:
             room.stop()
             del self._rooms[room_id]
 
+    # 测试用
+    @staticmethod
+    def __send_test_message(room):
+        room.send_message(Command.ADD_TEXT, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'timestamp': time.time(),
+            'authorName': 'xfgryujk',
+            'content': '我能吞下玻璃而不伤身体'
+        })
+        room.send_message(Command.ADD_TEXT, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'timestamp': time.time(),
+            'authorName': 'xfgryujk',
+            'content': "I can eat glass, it doesn't hurt me."
+        })
+        room.send_message(Command.ADD_VIP, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'authorName': 'xfgryujk',
+        })
+        room.send_message(Command.ADD_GIFT, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'authorName': 'xfgryujk',
+            'giftName': '礼花',
+            'giftNum': 1,
+            'totalCoin': 28000
+        })
+        room.send_message(Command.ADD_GIFT, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'authorName': 'xfgryujk',
+            'giftName': '节奏风暴',
+            'giftNum': 1,
+            'totalCoin': 100000
+        })
+        room.send_message(Command.ADD_GIFT, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'authorName': 'xfgryujk',
+            'giftName': '摩天大楼',
+            'giftNum': 1,
+            'totalCoin': 450000
+        })
+        room.send_message(Command.ADD_GIFT, {
+            'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@24w_24h.webp',
+            'authorName': 'xfgryujk',
+            'giftName': '小电视飞船',
+            'giftNum': 1,
+            'totalCoin': 1245000
+        })
+
 
 room_manager = RoomManager()
 
@@ -134,5 +205,5 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
             room_manager.del_client(self.room_id, self)
 
     # 测试用
-    def check_origin(self, origin):
-        return True
+    # def check_origin(self, origin):
+    #     return True
diff --git a/frontend/src/components/LegacyPaidMessage.vue b/frontend/src/components/LegacyPaidMessage.vue
new file mode 100644
index 0000000..6cee9bf
--- /dev/null
+++ b/frontend/src/components/LegacyPaidMessage.vue
@@ -0,0 +1,20 @@
+<template>
+  <yt-live-chat-legacy-paid-message-renderer>
+    <div id="author-photo" :style="`background-image: url(${avatarUrl})`"></div>
+    <div id="content">
+      <div id="event-text">{{title}}</div>
+      <div id="detail-text">{{content}}</div>
+    </div>
+  </yt-live-chat-legacy-paid-message-renderer>
+</template>
+
+<script>
+export default {
+  name: 'LegacyPaidMessage',
+  props: {
+    avatarUrl: String,
+    title: String,
+    content: String
+  }
+}
+</script>
diff --git a/frontend/src/components/PaidMessage.vue b/frontend/src/components/PaidMessage.vue
new file mode 100644
index 0000000..2314f1b
--- /dev/null
+++ b/frontend/src/components/PaidMessage.vue
@@ -0,0 +1,56 @@
+<template>
+  <yt-live-chat-paid-message-renderer>
+    <div id="header" :style="'background-color: ' + headerColor">
+      <div id="author-photo" :style="`background-image: url(${avatarUrl})`"></div>
+      <div id="header-content">
+        <div id="author-name">{{authorName}}</div>
+        <div id="purchase-amount">{{title}}</div>
+      </div>
+    </div>
+    <div id="content" :style="'background-color: ' + contentColor">
+      <div id="message" dir="auto">{{content}}</div>
+    </div>
+  </yt-live-chat-paid-message-renderer>
+</template>
+
+<script>
+let LEVEL_TO_HEADER_COLOR = [
+  'rgba(0,184,212,1)', // $2浅蓝
+  'rgba(255,176,0,1)', // $10黄
+  'rgba(245,91,0,1)', // $20橙
+  'rgba(208,0,0,1)' // $100红
+]
+let LEVEL_TO_CONTENT_COLOR = [
+  'rgba(0,229,255,1)', // $2浅蓝
+  'rgba(236,182,29,1)', // $10黄
+  'rgba(255,127,0,1)', // $20橙
+  'rgba(230,33,23,1)' // $100红
+]
+
+export default {
+  name: 'PaidMessage',
+  props: {
+    level: Number, // 高亮等级,决定颜色
+    avatarUrl: String,
+    authorName: String,
+    title: String,
+    content: String
+  },
+  computed: {
+    headerColor() {
+      if (this.level < 0)
+        return LEVEL_TO_HEADER_COLOR[0]
+      if (this.level >= LEVEL_TO_HEADER_COLOR.length)
+        return LEVEL_TO_HEADER_COLOR[LEVEL_TO_HEADER_COLOR.length - 1]
+      return LEVEL_TO_HEADER_COLOR[this.level]
+    },
+    contentColor() {
+      if (this.level < 0)
+        return LEVEL_TO_CONTENT_COLOR[0]
+      if (this.level >= LEVEL_TO_CONTENT_COLOR.length)
+        return LEVEL_TO_CONTENT_COLOR[LEVEL_TO_CONTENT_COLOR.length - 1]
+      return LEVEL_TO_CONTENT_COLOR[this.level]
+    }
+  }
+}
+</script>
diff --git a/frontend/src/components/Room.vue b/frontend/src/components/Room.vue
index 1ebefbf..5844db9 100644
--- a/frontend/src/components/Room.vue
+++ b/frontend/src/components/Room.vue
@@ -16,9 +16,16 @@
     </yt-live-chat-ticker-renderer> -->
     <yt-live-chat-item-list-renderer>
       <template v-for="message in messages">
-        <text-message :key="message.id"
+        <text-message :key="message.id" v-if="message.type == 0"
           :avatarUrl="message.avatarUrl" :time="message.time" :authorName="message.authorName" :content="message.content"
         ></text-message>
+        <legacy-paid-message :key="message.id" v-else-if="message.type == 1"
+          :avatarUrl="message.avatarUrl" :title="message.title" :content="message.content"
+        ></legacy-paid-message>
+        <paid-message :key="message.id" v-else
+          :level="message.level" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
+          :title="message.title" :content="message.content"
+        ></paid-message>
       </template>
     </yt-live-chat-item-list-renderer>
   </yt-live-chat-renderer>
@@ -26,11 +33,15 @@
 
 <script>
 import TextMessage from './TextMessage.vue'
+import LegacyPaidMessage from './LegacyPaidMessage.vue'
+import PaidMessage from './PaidMessage.vue'
 
 export default {
   name: 'Room',
   components: {
-    TextMessage
+    TextMessage,
+    LegacyPaidMessage,
+    PaidMessage
   },
   data() {
     return {
@@ -40,9 +51,9 @@ export default {
     }
   },
   created() {
-    // this.websocket = new WebSocket(`ws://${window.location.host}/chat`)
+    this.websocket = new WebSocket(`ws://${window.location.host}/chat`)
     // 测试用
-    this.websocket = new WebSocket('ws://localhost/chat')
+    // this.websocket = new WebSocket('ws://localhost/chat')
     this.websocket.onopen = () => this.websocket.send(JSON.stringify({
       cmd: 0, // JOIN_ROOM
       data: {
@@ -51,23 +62,57 @@ export default {
     }))
     this.websocket.onmessage = (event) => {
       let body = JSON.parse(event.data)
-      let time = new Date(body.data.timestamp * 1000)
-      let message = {
-        id: this.nextId++,
-        time: `${time.getHours()}:${time.getMinutes()}`,
-        ...body.data
-      }
+      let message = null
+      let time, price, level
       switch(body.cmd) {
         case 1: // ADD_TEXT
-          this.messages.push(message)
-          if (this.messages.length > 50)
-            this.messages.shift()
+          time = new Date(body.data.timestamp * 1000)
+          message = {
+            id: this.nextId++,
+            type: 0, // TextMessage
+            avatarUrl: body.data.avatarUrl,
+            time: `${time.getHours()}:${time.getMinutes()}`,
+            authorName: body.data.authorName,
+            content: body.data.content
+          }
           break;
         case 2: // ADD_GIFT
+          price = body.data.totalCoin / 1000
+          if (price < 9.9) // 丢人
+            break
+          else if (price < 100) // B坷垃~打call
+            level = 0
+          else if (price < 300) // 节奏风暴、天空之翼
+            level = 1
+          else if (price < 500) // 摩天大楼
+            level = 2
+          else // 小电视飞船
+            level = 3
+          message = {
+            id: this.nextId++,
+            type: 2, // PaidMessage
+            level: level,
+            avatarUrl: body.data.avatarUrl,
+            authorName: body.data.authorName,
+            title: `CNY${price}`,
+            content: `Sent ${body.data.giftName}x${body.data.giftNum}`
+          }
           break;
         case 3: // ADD_VIP
+          message = {
+            id: this.nextId++,
+            type: 1, // LegacyPaidMessage
+            avatarUrl: body.data.avatarUrl,
+            title: `NEW MEMBER!`,
+            content: `Welcome ${body.data.authorName}`
+          }
           break;
       }
+      if (message) {
+        this.messages.push(message)
+        if (this.messages.length > 50)
+          this.messages.shift()
+      }
     }
   },
   beforeDestroy() {