diff --git a/accounts/accounts.go b/accounts/accounts.go
index b367ee2f4b3562d79597f386bddf249259e2021b..6c9e0bbcee4cd530fb5c613a31ebe2f2c66e702b 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -29,19 +29,16 @@ import (
 // by the optional URL field.
 type Account struct {
 	Address common.Address `json:"address"` // Ethereum account address derived from the key
-	URL     string         `json:"url"`     // Optional resource locator within a backend
+	URL     URL            `json:"url"`     // Optional resource locator within a backend
 }
 
 // Wallet represents a software or hardware wallet that might contain one or more
 // accounts (derived from the same seed).
 type Wallet interface {
-	// Type retrieves a textual representation of the type of the wallet.
-	Type() string
-
 	// URL retrieves the canonical path under which this wallet is reachable. It is
 	// user by upper layers to define a sorting order over all wallets from multiple
 	// backends.
-	URL() string
+	URL() URL
 
 	// Status returns a textual status to aid the user in the current state of the
 	// wallet.
diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go
index cc8626afced6c8acfe5601b530b09bd5b606da65..e2f8262500b34e469d4bc008cef04ce7674a772e 100644
--- a/accounts/keystore/account_cache.go
+++ b/accounts/keystore/account_cache.go
@@ -42,7 +42,7 @@ const minReloadInterval = 2 * time.Second
 type accountsByURL []accounts.Account
 
 func (s accountsByURL) Len() int           { return len(s) }
-func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL }
+func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 }
 func (s accountsByURL) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
 
 // AmbiguousAddrError is returned when attempting to unlock
@@ -55,7 +55,7 @@ type AmbiguousAddrError struct {
 func (err *AmbiguousAddrError) Error() string {
 	files := ""
 	for i, a := range err.Matches {
-		files += a.URL
+		files += a.URL.Path
 		if i < len(err.Matches)-1 {
 			files += ", "
 		}
@@ -104,7 +104,7 @@ func (ac *accountCache) add(newAccount accounts.Account) {
 	ac.mu.Lock()
 	defer ac.mu.Unlock()
 
-	i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL })
+	i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 })
 	if i < len(ac.all) && ac.all[i] == newAccount {
 		return
 	}
@@ -155,10 +155,10 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) {
 	if (a.Address != common.Address{}) {
 		matches = ac.byAddr[a.Address]
 	}
-	if a.URL != "" {
+	if a.URL.Path != "" {
 		// If only the basename is specified, complete the path.
-		if !strings.ContainsRune(a.URL, filepath.Separator) {
-			a.URL = filepath.Join(ac.keydir, a.URL)
+		if !strings.ContainsRune(a.URL.Path, filepath.Separator) {
+			a.URL.Path = filepath.Join(ac.keydir, a.URL.Path)
 		}
 		for i := range matches {
 			if matches[i].URL == a.URL {
@@ -272,7 +272,7 @@ func (ac *accountCache) scan() ([]accounts.Account, error) {
 		case (addr == common.Address{}):
 			glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path)
 		default:
-			addrs = append(addrs, accounts.Account{Address: addr, URL: path})
+			addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
 		}
 		fd.Close()
 	}
diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go
index ea6f7d01189466340e9d1e332825903d9c720f28..3e68351ea5db7a91ace77e1ba044fd6694e50324 100644
--- a/accounts/keystore/account_cache_test.go
+++ b/accounts/keystore/account_cache_test.go
@@ -37,15 +37,15 @@ var (
 	cachetestAccounts = []accounts.Account{
 		{
 			Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
-			URL:     filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")},
 		},
 		{
 			Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
-			URL:     filepath.Join(cachetestDir, "aaa"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")},
 		},
 		{
 			Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
-			URL:     filepath.Join(cachetestDir, "zzz"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")},
 		},
 	}
 )
@@ -63,10 +63,11 @@ func TestWatchNewFile(t *testing.T) {
 	// Move in the files.
 	wantAccounts := make([]accounts.Account, len(cachetestAccounts))
 	for i := range cachetestAccounts {
-		a := cachetestAccounts[i]
-		a.URL = filepath.Join(dir, filepath.Base(a.URL))
-		wantAccounts[i] = a
-		if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil {
+		wantAccounts[i] = accounts.Account{
+			Address: cachetestAccounts[i].Address,
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))},
+		}
+		if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil {
 			t.Fatal(err)
 		}
 	}
@@ -107,13 +108,13 @@ func TestWatchNoDir(t *testing.T) {
 	os.MkdirAll(dir, 0700)
 	defer os.RemoveAll(dir)
 	file := filepath.Join(dir, "aaa")
-	if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil {
+	if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil {
 		t.Fatal(err)
 	}
 
 	// ks should see the account.
 	wantAccounts := []accounts.Account{cachetestAccounts[0]}
-	wantAccounts[0].URL = file
+	wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
 	for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
 		list = ks.Accounts()
 		if reflect.DeepEqual(list, wantAccounts) {
@@ -145,31 +146,31 @@ func TestCacheAddDeleteOrder(t *testing.T) {
 	accs := []accounts.Account{
 		{
 			Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
-			URL:     "-309830980",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"},
 		},
 		{
 			Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
-			URL:     "ggg",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"},
 		},
 		{
 			Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"),
-			URL:     "zzzzzz-the-very-last-one.keyXXX",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"},
 		},
 		{
 			Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
-			URL:     "SOMETHING.key",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"},
 		},
 		{
 			Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
-			URL:     "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"},
 		},
 		{
 			Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
-			URL:     "aaa",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"},
 		},
 		{
 			Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
-			URL:     "zzz",
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"},
 		},
 	}
 	for _, a := range accs {
@@ -210,7 +211,7 @@ func TestCacheAddDeleteOrder(t *testing.T) {
 	for i := 0; i < len(accs); i += 2 {
 		cache.delete(wantAccounts[i])
 	}
-	cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"})
+	cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}})
 
 	select {
 	case <-notify:
@@ -245,19 +246,19 @@ func TestCacheFind(t *testing.T) {
 	accs := []accounts.Account{
 		{
 			Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
-			URL:     filepath.Join(dir, "a.key"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")},
 		},
 		{
 			Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
-			URL:     filepath.Join(dir, "b.key"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")},
 		},
 		{
 			Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
-			URL:     filepath.Join(dir, "c.key"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")},
 		},
 		{
 			Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
-			URL:     filepath.Join(dir, "c2.key"),
+			URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")},
 		},
 	}
 	for _, a := range accs {
@@ -266,7 +267,7 @@ func TestCacheFind(t *testing.T) {
 
 	nomatchAccount := accounts.Account{
 		Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
-		URL:     filepath.Join(dir, "something"),
+		URL:     accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")},
 	}
 	tests := []struct {
 		Query      accounts.Account
@@ -278,7 +279,7 @@ func TestCacheFind(t *testing.T) {
 		// by file
 		{Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]},
 		// by basename
-		{Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]},
+		{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]},
 		// by file and address
 		{Query: accs[0], WantResult: accs[0]},
 		// ambiguous address, tie resolved by file
@@ -294,7 +295,7 @@ func TestCacheFind(t *testing.T) {
 		// no match error
 		{Query: nomatchAccount, WantError: ErrNoMatch},
 		{Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch},
-		{Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch},
+		{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch},
 		{Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
 	}
 	for i, test := range tests {
diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go
index 1cf5f9366281cc2c248aed5aaa1e83d03bac70cf..e2bdf09feec55013af4ee273f002fbb9aea9ec6c 100644
--- a/accounts/keystore/key.go
+++ b/accounts/keystore/key.go
@@ -181,8 +181,8 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou
 	if err != nil {
 		return nil, accounts.Account{}, err
 	}
-	a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))}
-	if err := ks.StoreKey(a.URL, key, auth); err != nil {
+	a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}}
+	if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
 		zeroKey(key.PrivateKey)
 		return nil, a, err
 	}
diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go
index ce4e87ce90f9c0f4a90e27711145e52f3a40ef5c..a01ff17e334408ded335c808ac02eb760854f20c 100644
--- a/accounts/keystore/keystore.go
+++ b/accounts/keystore/keystore.go
@@ -49,6 +49,9 @@ var (
 // KeyStoreType is the reflect type of a keystore backend.
 var KeyStoreType = reflect.TypeOf(&KeyStore{})
 
+// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs.
+var KeyStoreScheme = "keystore"
+
 // Maximum time between wallet refreshes (if filesystem notifications don't work).
 const walletRefreshCycle = 3 * time.Second
 
@@ -130,22 +133,21 @@ func (ks *KeyStore) Wallets() []accounts.Wallet {
 // necessary wallet refreshes.
 func (ks *KeyStore) refreshWallets() {
 	// Retrieve the current list of accounts
+	ks.mu.Lock()
 	accs := ks.cache.accounts()
 
 	// Transform the current list of wallets into the new one
-	ks.mu.Lock()
-
 	wallets := make([]accounts.Wallet, 0, len(accs))
 	events := []accounts.WalletEvent{}
 
 	for _, account := range accs {
 		// Drop wallets while they were in front of the next account
-		for len(ks.wallets) > 0 && ks.wallets[0].URL() < account.URL {
+		for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 {
 			events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false})
 			ks.wallets = ks.wallets[1:]
 		}
 		// If there are no more wallets or the account is before the next, wrap new wallet
-		if len(ks.wallets) == 0 || ks.wallets[0].URL() > account.URL {
+		if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 {
 			wallet := &keystoreWallet{account: account, keystore: ks}
 
 			events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
@@ -242,7 +244,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
 	// The order is crucial here. The key is dropped from the
 	// cache after the file is gone so that a reload happening in
 	// between won't insert it into the cache again.
-	err = os.Remove(a.URL)
+	err = os.Remove(a.URL.Path)
 	if err == nil {
 		ks.cache.delete(a)
 		ks.refreshWallets()
@@ -377,7 +379,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A
 	if err != nil {
 		return a, nil, err
 	}
-	key, err := ks.storage.GetKey(a.Address, a.URL, auth)
+	key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth)
 	return a, key, err
 }
 
@@ -453,8 +455,8 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
 }
 
 func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
-	a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))}
-	if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil {
+	a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
+	if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
 		return accounts.Account{}, err
 	}
 	ks.cache.add(a)
@@ -468,7 +470,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string)
 	if err != nil {
 		return err
 	}
-	return ks.storage.StoreKey(a.URL, key, newPassphrase)
+	return ks.storage.StoreKey(a.URL.Path, key, newPassphrase)
 }
 
 // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
diff --git a/accounts/keystore/keystore_plain_test.go b/accounts/keystore/keystore_plain_test.go
index dcb2ab901a27c2817b5b53ab47e7390f8a29d9a8..8c0eb52ea1fad943626beba324a270c4a6692ad0 100644
--- a/accounts/keystore/keystore_plain_test.go
+++ b/accounts/keystore/keystore_plain_test.go
@@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	k2, err := ks.GetKey(k1.Address, account.URL, pass)
+	k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	k2, err := ks.GetKey(k1.Address, account.URL, pass)
+	k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -94,7 +94,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if _, err = ks.GetKey(k1.Address, account.URL, "bar"); err != ErrDecrypt {
+	if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err != ErrDecrypt {
 		t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt)
 	}
 }
@@ -115,7 +115,7 @@ func TestImportPreSaleKey(t *testing.T) {
 	if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") {
 		t.Errorf("imported account has wrong address %x", account.Address)
 	}
-	if !strings.HasPrefix(account.URL, dir) {
+	if !strings.HasPrefix(account.URL.Path, dir) {
 		t.Errorf("imported account file not in keystore directory: %q", account.URL)
 	}
 }
diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go
index 6b7170a2fb444d5a6b88d27e0350243aa45c96b2..1f1935be60739c119f870d24288e57addc38dd4d 100644
--- a/accounts/keystore/keystore_test.go
+++ b/accounts/keystore/keystore_test.go
@@ -41,10 +41,10 @@ func TestKeyStore(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if !strings.HasPrefix(a.URL, dir) {
+	if !strings.HasPrefix(a.URL.Path, dir) {
 		t.Errorf("account file %s doesn't have dir prefix", a.URL)
 	}
-	stat, err := os.Stat(a.URL)
+	stat, err := os.Stat(a.URL.Path)
 	if err != nil {
 		t.Fatalf("account file %s doesn't exist (%v)", a.URL, err)
 	}
@@ -60,7 +60,7 @@ func TestKeyStore(t *testing.T) {
 	if err := ks.Delete(a, "bar"); err != nil {
 		t.Errorf("Delete error: %v", err)
 	}
-	if common.FileExist(a.URL) {
+	if common.FileExist(a.URL.Path) {
 		t.Errorf("account file %s should be gone after Delete", a.URL)
 	}
 	if ks.HasAddress(a.Address) {
@@ -286,7 +286,7 @@ func TestWalletNotifications(t *testing.T) {
 
 	// Randomly add and remove account and make sure events and wallets are in sync
 	live := make(map[common.Address]accounts.Account)
-	for i := 0; i < 1024; i++ {
+	for i := 0; i < 256; i++ {
 		// Execute a creation or deletion and ensure event arrival
 		if create := len(live) == 0 || rand.Int()%4 > 0; create {
 			// Add a new account and ensure wallet notifications arrives
@@ -349,8 +349,6 @@ func TestWalletNotifications(t *testing.T) {
 				}
 			}
 		}
-		// Sleep a bit to avoid same-timestamp keyfiles
-		time.Sleep(10 * time.Millisecond)
 	}
 }
 
diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go
index d92926478bfd8bef7ec76c1c527356a67fc5ed64..7d5507a4fc50539a0744a9c31ef7cd782f67d58f 100644
--- a/accounts/keystore/keystore_wallet.go
+++ b/accounts/keystore/keystore_wallet.go
@@ -30,20 +30,21 @@ type keystoreWallet struct {
 	keystore *KeyStore        // Keystore where the account originates from
 }
 
-// Type implements accounts.Wallet, returning the textual type of the wallet.
-func (w *keystoreWallet) Type() string {
-	return "secret-storage"
-}
-
 // URL implements accounts.Wallet, returning the URL of the account within.
-func (w *keystoreWallet) URL() string {
+func (w *keystoreWallet) URL() accounts.URL {
 	return w.account.URL
 }
 
 // Status implements accounts.Wallet, always returning "open", since there is no
 // concept of open/close for plain keystore accounts.
 func (w *keystoreWallet) Status() string {
-	return "Open"
+	w.keystore.mu.RLock()
+	defer w.keystore.mu.RUnlock()
+
+	if _, ok := w.keystore.unlocked[w.account.Address]; ok {
+		return "Unlocked"
+	}
+	return "Locked"
 }
 
 // Open implements accounts.Wallet, but is a noop for plain wallets since there
@@ -63,7 +64,7 @@ func (w *keystoreWallet) Accounts() []accounts.Account {
 // Contains implements accounts.Wallet, returning whether a particular account is
 // or is not wrapped by this wallet instance.
 func (w *keystoreWallet) Contains(account accounts.Account) bool {
-	return account.Address == w.account.Address && (account.URL == "" || account.URL == w.account.URL)
+	return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL)
 }
 
 // Derive implements accounts.Wallet, but is a noop for plain wallets since there
@@ -81,7 +82,7 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte
 	if account.Address != w.account.Address {
 		return nil, accounts.ErrUnknownAccount
 	}
-	if account.URL != "" && account.URL != w.account.URL {
+	if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
 		return nil, accounts.ErrUnknownAccount
 	}
 	// Account seems valid, request the keystore to sign
@@ -97,7 +98,7 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction,
 	if account.Address != w.account.Address {
 		return nil, accounts.ErrUnknownAccount
 	}
-	if account.URL != "" && account.URL != w.account.URL {
+	if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
 		return nil, accounts.ErrUnknownAccount
 	}
 	// Account seems valid, request the keystore to sign
@@ -111,7 +112,7 @@ func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passph
 	if account.Address != w.account.Address {
 		return nil, accounts.ErrUnknownAccount
 	}
-	if account.URL != "" && account.URL != w.account.URL {
+	if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
 		return nil, accounts.ErrUnknownAccount
 	}
 	// Account seems valid, request the keystore to sign
@@ -125,7 +126,7 @@ func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphra
 	if account.Address != w.account.Address {
 		return nil, accounts.ErrUnknownAccount
 	}
-	if account.URL != "" && account.URL != w.account.URL {
+	if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
 		return nil, accounts.ErrUnknownAccount
 	}
 	// Account seems valid, request the keystore to sign
diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go
index 948320e0a83f37f402f80628de317fec49a6ac53..5b883c45fab0d9ed621c9dfd56ac47a0c0cd351e 100644
--- a/accounts/keystore/presale.go
+++ b/accounts/keystore/presale.go
@@ -38,8 +38,8 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou
 		return accounts.Account{}, nil, err
 	}
 	key.Id = uuid.NewRandom()
-	a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))}
-	err = keyStore.StoreKey(a.URL, key, password)
+	a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}}
+	err = keyStore.StoreKey(a.URL.Path, key, password)
 	return a, key, err
 }
 
diff --git a/accounts/manager.go b/accounts/manager.go
index 0822500eb6f1a6f952e6364aae287d6dfdf30174..12a5bfcd9009991bc827aa31af35a4974ff87f35 100644
--- a/accounts/manager.go
+++ b/accounts/manager.go
@@ -134,8 +134,12 @@ func (am *Manager) Wallet(url string) (Wallet, error) {
 	am.lock.RLock()
 	defer am.lock.RUnlock()
 
+	parsed, err := parseURL(url)
+	if err != nil {
+		return nil, err
+	}
 	for _, wallet := range am.Wallets() {
-		if wallet.URL() == url {
+		if wallet.URL() == parsed {
 			return wallet, nil
 		}
 	}
@@ -169,7 +173,7 @@ func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription {
 // The original slice is assumed to be already sorted by URL.
 func merge(slice []Wallet, wallets ...Wallet) []Wallet {
 	for _, wallet := range wallets {
-		n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() })
+		n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 })
 		if n == len(slice) {
 			slice = append(slice, wallet)
 			continue
@@ -183,7 +187,7 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet {
 // cache and removes the ones specified.
 func drop(slice []Wallet, wallets ...Wallet) []Wallet {
 	for _, wallet := range wallets {
-		n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() })
+		n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 })
 		if n == len(slice) {
 			// Wallet not found, may happen during startup
 			continue
diff --git a/accounts/url.go b/accounts/url.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2d00c1c6217a7a09503778ae6971a6ead568ef7
--- /dev/null
+++ b/accounts/url.go
@@ -0,0 +1,79 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// URL represents the canonical identification URL of a wallet or account.
+//
+// It is a simplified version of url.URL, with the important limitations (which
+// are considered features here) that it contains value-copyable components only,
+// as well as that it doesn't do any URL encoding/decoding of special characters.
+//
+// The former is important to allow an account to be copied without leaving live
+// references to the original version, whereas the latter is important to ensure
+// one single canonical form opposed to many allowed ones by the RFC 3986 spec.
+//
+// As such, these URLs should not be used outside of the scope of an Ethereum
+// wallet or account.
+type URL struct {
+	Scheme string // Protocol scheme to identify a capable account backend
+	Path   string // Path for the backend to identify a unique entity
+}
+
+// parseURL converts a user supplied URL into the accounts specific structure.
+func parseURL(url string) (URL, error) {
+	parts := strings.Split(url, "://")
+	if len(parts) != 2 || parts[0] == "" {
+		return URL{}, errors.New("protocol scheme missing")
+	}
+	return URL{
+		Scheme: parts[0],
+		Path:   parts[1],
+	}, nil
+}
+
+// String implements the stringer interface.
+func (u URL) String() string {
+	if u.Scheme != "" {
+		return fmt.Sprintf("%s://%s", u.Scheme, u.Path)
+	}
+	return u.Path
+}
+
+// MarshalJSON implements the json.Marshaller interface.
+func (u URL) MarshalJSON() ([]byte, error) {
+	return json.Marshal(u.String())
+}
+
+// Cmp compares x and y and returns:
+//
+//   -1 if x <  y
+//    0 if x == y
+//   +1 if x >  y
+//
+func (u URL) Cmp(url URL) int {
+	if u.Scheme == url.Scheme {
+		return strings.Compare(u.Path, url.Path)
+	}
+	return strings.Compare(u.Scheme, url.Scheme)
+}
diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go
index ebd6fddb40f538967a57d4006d72ec21be728b9e..bd397249f1bee488fd5312af944fdd4ea2e266dd 100644
--- a/accounts/usbwallet/ledger_hub.go
+++ b/accounts/usbwallet/ledger_hub.go
@@ -32,6 +32,9 @@ import (
 	"github.com/karalabe/gousb/usb"
 )
 
+// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
+var LedgerScheme = "ledger"
+
 // ledgerDeviceIDs are the known device IDs that Ledger wallets use.
 var ledgerDeviceIDs = []deviceID{
 	{Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue
@@ -124,23 +127,24 @@ func (hub *LedgerHub) refreshWallets() {
 
 	for i := 0; i < len(devIDs); i++ {
 		devID, busID := devIDs[i], busIDs[i]
-		url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff)
+
+		url := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)}
 
 		// Drop wallets while they were in front of the next account
-		for len(hub.wallets) > 0 && hub.wallets[0].URL() < url {
+		for len(hub.wallets) > 0 && hub.wallets[0].URL().Cmp(url) < 0 {
 			events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false})
 			hub.wallets = hub.wallets[1:]
 		}
 		// If there are no more wallets or the account is before the next, wrap new wallet
-		if len(hub.wallets) == 0 || hub.wallets[0].URL() > url {
-			wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url}
+		if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
+			wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: &url}
 
 			events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
 			wallets = append(wallets, wallet)
 			continue
 		}
 		// If the account is the same as the first wallet, keep it
-		if hub.wallets[0].URL() == url {
+		if hub.wallets[0].URL().Cmp(url) == 0 {
 			wallets = append(wallets, hub.wallets[0])
 			hub.wallets = hub.wallets[1:]
 			continue
diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go
index 35e671c46ac3b0fc3695e139fdb15cd7bd70721d..f712a503f42137efdf824342775e3f6ab9f7c341 100644
--- a/accounts/usbwallet/ledger_wallet.go
+++ b/accounts/usbwallet/ledger_wallet.go
@@ -72,10 +72,10 @@ const (
 
 // ledgerWallet represents a live USB Ledger hardware wallet.
 type ledgerWallet struct {
-	context    *usb.Context // USB context to interface libusb through
-	hardwareID deviceID     // USB identifiers to identify this device type
-	locationID uint16       // USB bus and address to identify this device instance
-	url        string       // Textual URL uniquely identifying this wallet
+	context    *usb.Context  // USB context to interface libusb through
+	hardwareID deviceID      // USB identifiers to identify this device type
+	locationID uint16        // USB bus and address to identify this device instance
+	url        *accounts.URL // Textual URL uniquely identifying this wallet
 
 	device  *usb.Device  // USB device advertising itself as a Ledger wallet
 	input   usb.Endpoint // Input endpoint to send data to this device
@@ -90,14 +90,9 @@ type ledgerWallet struct {
 	lock sync.RWMutex
 }
 
-// Type implements accounts.Wallet, returning the textual type of the wallet.
-func (w *ledgerWallet) Type() string {
-	return "ledger"
-}
-
 // URL implements accounts.Wallet, returning the URL of the Ledger device.
-func (w *ledgerWallet) URL() string {
-	return w.url
+func (w *ledgerWallet) URL() accounts.URL {
+	return *w.url
 }
 
 // Status implements accounts.Wallet, always whether the Ledger is opened, closed
@@ -113,9 +108,9 @@ func (w *ledgerWallet) Status() string {
 		return "Closed"
 	}
 	if w.version == [3]byte{0, 0, 0} {
-		return "Ethereum app not started"
+		return "Ethereum app offline"
 	}
-	return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2])
+	return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
 }
 
 // Open implements accounts.Wallet, attempting to open a USB connection to the
@@ -309,7 +304,7 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) {
 	}
 	account := accounts.Account{
 		Address: address,
-		URL:     fmt.Sprintf("%s/%s", w.url, path),
+		URL:     accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
 	}
 	// If pinning was requested, track the account
 	if pin {
diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go
index 97a060e4851d29bfd009d9eebe7170d93a75948c..cd398eadb1405daec3472ae4f623d3ec3e4fe490 100644
--- a/cmd/geth/accountcmd.go
+++ b/cmd/geth/accountcmd.go
@@ -185,7 +185,7 @@ func accountList(ctx *cli.Context) error {
 	var index int
 	for _, wallet := range stack.AccountManager().Wallets() {
 		for _, account := range wallet.Accounts() {
-			fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, account.URL)
+			fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
 			index++
 		}
 	}
diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go
index 7e03d7548d57dd865892751d1b3350a737ddcb58..679a7ec30ee73bea5ab00543ab1e88586c5a3ccb 100644
--- a/cmd/geth/accountcmd_test.go
+++ b/cmd/geth/accountcmd_test.go
@@ -53,15 +53,15 @@ func TestAccountList(t *testing.T) {
 	defer geth.expectExit()
 	if runtime.GOOS == "windows" {
 		geth.expect(`
-Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
-Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}\keystore\aaa
-Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}\keystore\zzz
+Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
+Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}\keystore\aaa
+Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}\keystore\zzz
 `)
 	} else {
 		geth.expect(`
-Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
-Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}/keystore/aaa
-Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}/keystore/zzz
+Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
+Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}/keystore/aaa
+Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}/keystore/zzz
 `)
 	}
 }
@@ -247,12 +247,12 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
 !! Unsupported terminal, password will be echoed.
 Passphrase: {{.InputLine "foobar"}}
 Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
-   {{keypath "1"}}
-   {{keypath "2"}}
+   keystore://{{keypath "1"}}
+   keystore://{{keypath "2"}}
 Testing your passphrase against all of them...
-Your passphrase unlocked {{keypath "1"}}
+Your passphrase unlocked keystore://{{keypath "1"}}
 In order to avoid this warning, you need to remove the following duplicate key files:
-   {{keypath "2"}}
+   keystore://{{keypath "2"}}
 `)
 	geth.expectExit()
 
@@ -283,8 +283,8 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
 !! Unsupported terminal, password will be echoed.
 Passphrase: {{.InputLine "wrong"}}
 Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
-   {{keypath "1"}}
-   {{keypath "2"}}
+   keystore://{{keypath "1"}}
+   keystore://{{keypath "2"}}
 Testing your passphrase against all of them...
 Fatal: None of the listed files could be unlocked.
 `)
diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go
index a7fc72d57a0bc74184cc0d4b24038b3ec702bde7..f9d661bb3d8561c28e71d9e5f38a7c59b88fdf16 100644
--- a/cmd/swarm/main.go
+++ b/cmd/swarm/main.go
@@ -351,7 +351,7 @@ func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKe
 	if err != nil {
 		utils.Fatalf("Can't find swarm account key: %v", err)
 	}
-	keyjson, err := ioutil.ReadFile(a.URL)
+	keyjson, err := ioutil.ReadFile(a.URL.Path)
 	if err != nil {
 		utils.Fatalf("Can't load swarm account key: %v", err)
 	}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 0e75eb0e063be766a2150157b4d5bbb34875dc93..85bb10f17442bd0cfc6c15d3cf1367a3d6c6b797 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -228,7 +228,6 @@ func (s *PrivateAccountAPI) ListAccounts() []common.Address {
 // rawWallet is a JSON representation of an accounts.Wallet interface, with its
 // data contents extracted into plain fields.
 type rawWallet struct {
-	Type     string             `json:"type"`
 	URL      string             `json:"url"`
 	Status   string             `json:"status"`
 	Accounts []accounts.Account `json:"accounts"`
@@ -239,8 +238,7 @@ func (s *PrivateAccountAPI) ListWallets() []rawWallet {
 	var wallets []rawWallet
 	for _, wallet := range s.am.Wallets() {
 		wallets = append(wallets, rawWallet{
-			Type:     wallet.Type(),
-			URL:      wallet.URL(),
+			URL:      wallet.URL().String(),
 			Status:   wallet.Status(),
 			Accounts: wallet.Accounts(),
 		})
diff --git a/mobile/accounts.go b/mobile/accounts.go
index 897549a6b918345cd99ece377f87925360e085e5..fbaa3bf40e854fc4ef332fe2641c1dd9dc1bd0d7 100644
--- a/mobile/accounts.go
+++ b/mobile/accounts.go
@@ -78,9 +78,9 @@ func (a *Account) GetAddress() *Address {
 	return &Address{a.account.Address}
 }
 
-// GetFile retrieves the path of the file containing the account key.
-func (a *Account) GetFile() string {
-	return a.account.URL
+// GetURL retrieves the canonical URL of the account.
+func (a *Account) GetURL() string {
+	return a.account.URL.String()
 }
 
 // KeyStore manages a key storage directory on disk.
diff --git a/node/config.go b/node/config.go
index 9dc1642a59dd9cc6b97d6f7a2211618bd59b217e..47ecd22a33489db1d8fe18a5c430b889a0aaee75 100644
--- a/node/config.go
+++ b/node/config.go
@@ -454,12 +454,12 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
 	go func() {
 		for event := range changes {
 			if event.Arrive {
-				glog.V(logger.Info).Infof("New %s wallet appeared: %s", event.Wallet.Type(), event.Wallet.URL())
+				glog.V(logger.Info).Infof("New wallet appeared: %s", event.Wallet.URL())
 				if err := event.Wallet.Open(""); err != nil {
-					glog.V(logger.Warn).Infof("Failed to open %s wallet %s: %v", event.Wallet.Type(), event.Wallet.URL(), err)
+					glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", event.Wallet.URL(), err)
 				}
 			} else {
-				glog.V(logger.Info).Infof("Old %s wallet disappeared: %s", event.Wallet.Type(), event.Wallet.URL())
+				glog.V(logger.Info).Infof("Old wallet disappeared: %s", event.Wallet.URL())
 			}
 		}
 	}()