Index: trunk/downstream.go
===================================================================
--- trunk/downstream.go	(revision 116)
+++ trunk/downstream.go	(revision 117)
@@ -327,13 +327,19 @@
 	switch msg.Command {
 	case "NICK":
-		if err := parseMessageParams(msg, &dc.nick); err != nil {
-			return err
-		}
+		var nick string
+		if err := parseMessageParams(msg, &nick); err != nil {
+			return err
+		}
+		if nick == serviceNick {
+			return ircError{&irc.Message{
+				Command: irc.ERR_NICKNAMEINUSE,
+				Params:  []string{dc.nick, nick, "Nickname reserved for bouncer service"},
+			}}
+		}
+		dc.nick = nick
 	case "USER":
-		var username string
-		if err := parseMessageParams(msg, &username, nil, nil, &dc.realname); err != nil {
-			return err
-		}
-		dc.rawUsername = username
+		if err := parseMessageParams(msg, &dc.rawUsername, nil, nil, &dc.realname); err != nil {
+			return err
+		}
 	case "PASS":
 		if err := parseMessageParams(msg, &dc.password); err != nil {
@@ -891,4 +897,9 @@
 
 		for _, name := range strings.Split(targetsStr, ",") {
+			if name == serviceNick {
+				handleServicePRIVMSG(dc, text)
+				continue
+			}
+
 			uc, upstreamName, err := dc.unmarshalChannel(name)
 			if err != nil {
Index: trunk/go.mod
===================================================================
--- trunk/go.mod	(revision 116)
+++ trunk/go.mod	(revision 117)
@@ -5,4 +5,5 @@
 require (
 	github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
+	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4
Index: trunk/go.sum
===================================================================
--- trunk/go.sum	(revision 116)
+++ trunk/go.sum	(revision 117)
@@ -3,4 +3,6 @@
 github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
 github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
 github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
Index: trunk/service.go
===================================================================
--- trunk/service.go	(revision 117)
+++ trunk/service.go	(revision 117)
@@ -0,0 +1,87 @@
+package soju
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/google/shlex"
+	"gopkg.in/irc.v3"
+)
+
+const serviceNick = "BouncerServ"
+
+type serviceCommand struct {
+	usage  string
+	desc   string
+	handle func(dc *downstreamConn, params []string) error
+}
+
+func sendServicePRIVMSG(dc *downstreamConn, text string) {
+	dc.SendMessage(&irc.Message{
+		Prefix:  &irc.Prefix{Name: serviceNick},
+		Command: "PRIVMSG",
+		Params:  []string{dc.nick, text},
+	})
+}
+
+func handleServicePRIVMSG(dc *downstreamConn, text string) {
+	words, err := shlex.Split(text)
+	if err != nil {
+		sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
+		return
+	}
+
+	var name string
+	var params []string
+	if len(words) > 0 {
+		name = strings.ToLower(words[0])
+		params = words[1:]
+	}
+
+	cmd, ok := serviceCommands[name]
+	if !ok {
+		sendServicePRIVMSG(dc, fmt.Sprintf(`error: unknown command %q (type "help" for a list of commands)`, name))
+		return
+	}
+
+	if err := cmd.handle(dc, params); err != nil {
+		sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
+	}
+}
+
+var serviceCommands map[string]serviceCommand
+
+func init() {
+	serviceCommands = map[string]serviceCommand{
+		"help": {
+			usage:  "[command]",
+			desc:   "print help message",
+			handle: handleServiceHelp,
+		},
+	}
+}
+
+func handleServiceHelp(dc *downstreamConn, params []string) error {
+	if len(params) > 0 {
+		name := strings.ToLower(params[0])
+		cmd, ok := serviceCommands[name]
+		if !ok {
+			return fmt.Errorf("unknown command %q", name)
+		}
+
+		text := name
+		if cmd.usage != "" {
+			text += " " + cmd.usage
+		}
+		text += ": " + cmd.desc
+
+		sendServicePRIVMSG(dc, text)
+	} else {
+		var l []string
+		for name := range serviceCommands {
+			l = append(l, name)
+		}
+		sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
+	}
+	return nil
+}
Index: trunk/upstream.go
===================================================================
--- trunk/upstream.go	(revision 116)
+++ trunk/upstream.go	(revision 117)
@@ -559,7 +559,22 @@
 		})
 	case "PRIVMSG":
-		if err := parseMessageParams(msg, nil, nil); err != nil {
-			return err
-		}
+		if msg.Prefix == nil {
+			return fmt.Errorf("expected a prefix")
+		}
+
+		var nick string
+		if err := parseMessageParams(msg, &nick, nil); err != nil {
+			return err
+		}
+
+		if msg.Prefix.Name == serviceNick {
+			uc.logger.Printf("skipping PRIVMSG from soju's service: %v", msg)
+			break
+		}
+		if nick == serviceNick {
+			uc.logger.Printf("skipping PRIVMSG to soju's service: %v", msg)
+			break
+		}
+
 		uc.ring.Produce(msg)
 	case "INVITE":
