diff --git a/.gitignore b/.gitignore
index d9a84d8a68bb72dcd95e39d300b55eddef2c7e98..3e24356fae49d2010ee9c2b4075f40faab78503a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,5 +10,7 @@ test2network*/node
 test2network*/bootnode
 test2network/*.pid
 node_modules
+bin/venv
 src/*/*.bin
 src/*/*.abi
+__pycache__
diff --git a/bin/libbfa/__README__.md b/bin/libbfa/__README__.md
new file mode 100755
index 0000000000000000000000000000000000000000..e5ccf3b28317b36d6e641d9deb80f2f7a45d3b9a
--- /dev/null
+++ b/bin/libbfa/__README__.md
@@ -0,0 +1,128 @@
+# libbfa.py
+
+## Library for Python3 to talk to your local POA node.
+
+```python
+#!/usr/bin/env python3
+
+import libbfa
+
+bee = '0xbee0966BdC4568726AB3d3131b02e6255e29285D'
+d18 = '0xbfA2c97c3f59cc929e8fEB1aEE2Aca0B38235d18'
+```
+Which network do you want to connect to?  
+If you want to connect to an open/public node
+you can specify `bfa2018` or `test2`. If you
+want to connect to another (your own?) node,
+you can specify the URL, or give a `Provider`
+```python
+bfa = libbfa.Bfa('test2')
+bfa = libbfa.Bfa('http://localhost:8545')
+```
+Find the file locally which matches the name
+mentioned in the first argument. '0x' is
+removed before case-insensitive matching
+is performed.  
+Second argument (if specified) is the password.
+```python
+acct = libbfa.Account(bee)
+acct = libbfa.Account(bee, 'pepe')
+```
+Create a skeleton for the 'factory'  
+The name of the contract is given in the second
+argument and must be a contract in the current
+directory (not symlinked).  
+If the contract is not compiled locally already
+your local docker installation is used (must have).  
+```python
+Factory = libbfa.CompiledContract(bfa.w3, 'Majority')
+```
+### Deploy a new contract.
+
+If no address is given, an
+account (object) must be given, which will be used
+to deploy a new instance of the contract on the
+blockchain. *You usually only want to deploy a
+contract one time.* (If you need multiple tries,
+consider using the test-net).
+
+If you choose to deploy a new contract, remember to
+give the arguments that the contract wishes for it's
+constructor **or you will get an absurd error message
+about being out of gas**.
+
+The number `86400` in this example is the desired
+argument for deployment of this particular contract
+that the smart contract will get passed to it's
+constructor. This contract takes a `uint256`.
+
+Once you succesfully have a deployed contract in the
+network, you can find it's address in the receipt's
+`.address` field. Make a note of that address,
+because you must use that address for all
+subsequent calls to reach that contract.
+
+```python
+newdeployment = Factory.instance(86400, account=acct)
+contractaddress = newdeployment.address
+print('Your contract is deployed at address ' + contractaddress)
+newdeployment = None
+```
+
+Now it is deployed and we can pretend that happened
+many days ago. The 4 lines above need not be repeated,
+but the others do (libbfa, account and Factory setup).
+
+Now, we'll create a new reference to the same contract.
+
+Since our contract is deployed now, we
+see how to reference the already deployed
+contract.
+```python
+samecontract = Factory.instance(address=contractaddress)
+# better variable name for later
+majority=samecontract
+```
+call()s are free (no gas needed) and local to the node
+you are working with.
+They require no account, no signature, no
+transaction and are almost instant.
+```python
+print('Council length: {}'.format(majority.functions.councilLength().call()))
+print('Votes length: {}'.format(majority.functions.votesLength().call()))
+print('isCouncil bee: {}'.format(majority.functions.isCouncil(bee).call()))
+print('mayVote bee,d18,True: {}'.format(majority.functions.mayVote(bee,d18,True).call()))
+print('mayVote bee,d18,False: {}'.format(majority.functions.mayVote(bee,d18,False).call()))
+print('mayVote d18,bee,True: {}'.format(majority.functions.mayVote(d18,bee,True).call()))
+```
+Transactions form part of the blockchain and must be mined
+by the sealers, so they take a little longer to complete.
+They are signed by the sender account.
+
+Your *kwargs* must reference a configured `web3` (probably the one from
+`libbfa`), and also give a reference to the `function` that you are
+going to send a transaction to.
+
+The first arguments (all the *args*) will be sent to the function in
+the smart contract, so make sure the match in type.
+
+The function name is a funny mix between Python variable names you
+have defined yourself, plus functions which have arisen from the smart
+contracts ABI.
+
+```python
+r = acct.transact(d18, False, web3=bfa.w3, function=majority.functions.vote)
+print(r)
+```
+
+### Error examples
+
+<a name="argument-after-asterisk-must-be-an-iterable-not-nonetype"></a>
+argument after * must be an iterable, not NoneType
+
+```python
+# Error text: argument after * must be an iterable, not NoneType
+print(majorcontr.functions.councilLength.call())
+# Fix
+print(majorcontr.functions.councilLength().call())
+```
diff --git a/bin/libbfa/__init__.py b/bin/libbfa/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..e22c7f41188f07df10965869913ebb2fcf5b1426
--- /dev/null
+++ b/bin/libbfa/__init__.py
@@ -0,0 +1,328 @@
+import os
+import sys
+import subprocess
+import json
+import re
+import web3
+import web3.exceptions
+import web3.middleware
+import ecdsa
+from Crypto.Hash import keccak
+
+
+class Bfa:
+
+    def __init__(self, provider=''):
+        if 'BFAHOME' not in os.environ:
+            if os.path.isdir('/home/bfa/bfa'):
+                os.putenv('BFAHOME', '/home/bfa/bfa')
+            elif 'HOME' in os.environ and os.path.isdir(os.path.join(os.environ['HOME'], 'bfa')):
+                os.putenv('BFAHOME', os.path.join(os.environ['HOME'], 'bfa'))
+        if isinstance(provider, str):
+            if provider in ['prod', 'bfa2018' 'network']:
+                provider = web3.HTTPProvider("http://public.bfa.ar:8545/")
+            elif provider in ['test2network', 'test2', 'test', '']:
+                provider = web3.HTTPProvider("http://public.test2.bfa.ar:8545/")
+            elif provider.startswith('http://') or provider.startswith('https://'):
+                provider = web3.HTTPProvider(provider)
+            else:
+                raise ValueError('I do not know how to handle that provider.')
+        w3 = web3.Web3(provider)
+        # inject POA compatibility middleware
+        w3.middleware_onion.inject(web3.middleware.geth_poa_middleware, layer=0)
+        self.w3 = w3
+
+
+class Account:
+
+    def __init__(self, accountname: str, passphrase: str = ''):
+        self.keyfile = None
+        self.privatekey = None
+        self.unlock(accountname, passphrase)
+        self.nonce = 0
+
+    def __repr__(self) -> str:
+        return str(dict(keyfile=self.keyfile, privatekey=str(self.privatekey)))
+
+    def __str__(self) -> str:
+        return self.walletaddress()
+
+    @staticmethod
+    def findkeyfilesindirectories(pattern: str):
+        # Remove leading 0x if present
+        if pattern.startswith('0x'):
+            pattern = pattern[2:]
+        # Lower case the pattern (account name)
+        pattern = pattern.lower()
+        # Find candidate directories of where to look for accounts
+        wheretolook = []
+        if os.getenv('BFANODEDIR'):
+            wheretolook += [os.path.join(os.environ['BFANODEDIR'], 'keystore')]
+        elif os.getenv('BFANETWORKDIR'):
+            wheretolook += [os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore')]
+        elif os.getenv('BFAHOME'):
+            wheretolook += [os.path.join(os.environ['BFANETWORKDIR'], 'network', 'node', 'keystore')]
+        if os.getenv('HOME'):
+            wheretolook += [
+                os.path.join(os.environ['HOME'], '.ethereum', 'keystore'),
+                os.path.join(os.environ['HOME'], '.ethereum', 'keystore', 'test2'),
+                os.path.join(os.environ['HOME'], '.ethereum', 'keystore', 'network')
+            ]
+        # Look for our pattern (or all files) in the directories
+        matches = []
+        ourregexp = '.*{}$'.format(pattern)
+        for d in wheretolook:
+            if os.path.exists(d):
+                for f in os.listdir(d):
+                    fn = os.path.join(d, f)
+                    if os.path.isfile(fn):
+                        if re.match(ourregexp, f.lower()):
+                            matches += [os.path.join(d, f)]
+        if pattern:
+            if len(matches) == 0:
+                # if a pattern was given but no matches were found,
+                # return None
+                return None
+            else:
+                # if a pattern was found but some matches were found,
+                # return just the first one
+                return matches[0]
+        # If no pattern was given, return everything found, or the
+        # empty list.
+        return matches
+
+    def unlock(self, accountname: str, passphrase: str):
+        if os.path.isfile(accountname):
+            self.keyfile = accountname
+        else:
+            self.keyfile = self.findkeyfilesindirectories(accountname)
+        if self.keyfile is None:
+            raise FileNotFoundError('The account was not found.')
+        with open(self.keyfile) as fd:
+            encrypted_key = fd.read()
+            try:
+                self.privatekey = web3.Web3().eth.account.decrypt(encrypted_key, passphrase)
+            except ValueError as exc:
+                raise ValueError(
+                    'The passphrase given for the account is incorrect, '
+                    'or the input file is not a valid key file.'
+                ) from exc
+
+    def publickey(self) -> bytearray:
+        key = ecdsa.SigningKey.from_string(self.privatekey, curve=ecdsa.SECP256k1).verifying_key
+        # returns bytestring
+        return key.to_string()
+
+    def walletaddress(self) -> str:
+        ourhash = keccak.new(digest_bits=256)
+        ourhash.update(self.publickey())
+        digest = ourhash.hexdigest()
+        return web3.Web3().toChecksumAddress('0x' + digest[-40:])
+
+    def transact(self, *args, **kwargs):
+        w3 = kwargs.get('web3')
+        afunction = kwargs.get('function')
+        tx_details = self.calculate_tx_details(w3, afunction, *args)
+        txobj = afunction(*args).buildTransaction(tx_details)
+        receipt = self.txsignsendwait(w3, txobj)
+        return receipt
+
+    def txsignsendwait(self, w3: web3.Web3, txobj: dict):
+        signedobj = self.signtx(txobj)
+        txhashbytes = w3.eth.sendRawTransaction(signedobj.rawTransaction)
+        self.nonce = self.nonce + 1
+        receipt = w3.eth.waitForTransactionReceipt(txhashbytes)
+        return receipt
+
+    def signtx(self, tx: dict):
+        signed = web3.Web3().eth.account.sign_transaction(tx, self.privatekey)
+        return signed
+
+    def calculate_tx_details(self, w3: web3.Web3, afunction, *args) -> dict:
+        # Nonce may have increase on the network without us noticing
+        # or past transactions may not yet have been mined (and a flooded
+        # txpool).
+        # This is a resonable fix (try not to send too many transactions)
+        # If you use waitForTransactionReceipt() between each transaction
+        # you will not have problems because of this.
+        self.nonce = max(self.nonce, w3.eth.getTransactionCount(self.walletaddress()))
+        # Set minimum gasPrice to 1 Gwei, but allow more if the network says so.
+        details = {
+            'chainId': w3.eth.chain_id(),
+            'gasPrice': min(w3.toWei('1', 'gwei'), w3.eth.gasPrice),
+            'nonce': self.nonce,
+            'from': self.walletaddress(),
+        }
+        # Ask for balance, so we can tell it, in case we have an exception
+        balance = w3.eth.getBalance(self.walletaddress())
+        try:
+            # Ask a node how much gas it would cost to deploy
+            gas = afunction(*args).estimateGas(details)
+        except web3.exceptions.SolidityError as exc:
+            raise web3.exceptions.SolidityError(
+                'The Ethereum Virtual Machine probably did not like that.'
+            ) from exc
+        except ValueError as exc:
+            raise ValueError(
+                'Your account may not have enough balance to work on this '
+                'network. It currently has {} wei.'
+                .format(balance)) from exc
+        details['gas'] = gas
+        return details
+
+
+class Abi(list):
+
+    def __str__(self):
+        txt = ''
+        for i in range(len(self)):
+            elem = self.__getitem__(i)
+            if type(elem) is not dict:
+                continue
+            if 'type' in elem:
+                txt += "({}) ".format(elem['type'])
+            name = ""
+            if 'name' in elem:
+                name = elem['name']
+            if 'inputs' in elem:
+                inputlist = list()
+                for inputnum in range(len(elem['inputs'])):
+                    _input = elem['inputs'][inputnum]
+                    args = _input['type']
+                    if 'name' in _input and _input['name'] != '':
+                        args = "{}: {}".format(_input['name'], args)
+                    inputlist.append(args)
+                txt += "{}({})".format(name, ', '.join(inputlist))
+            if 'outputs' in elem:
+                outputlist = list()
+                for outputnum in range(len(elem['outputs'])):
+                    output = elem['outputs'][outputnum]
+                    if 'name' in output and output['name'] != '':
+                        outputlist.append("{}: {}".format(output['name'], output['type']))
+                    else:
+                        outputlist.append("{}".format(output['type']))
+                txt += " -> ({})".format(', '.join(outputlist))
+            if 'stateMutability' in elem:
+                txt += ' [{}]'.format(elem['stateMutability'])
+            txt += "\n"
+        return txt
+        # print(txt, file=sys.stderr)
+        # return super(Abi, self).__str__()
+
+
+class CompiledContract:
+
+    solc_features = [
+        'abi', 'asm', 'ast', 'bin', 'bin-runtime', 'compact-format',
+        'devdoc', 'generated-sources', 'generated-sources-runtime',
+        'hashes', 'interface', 'metadata', 'opcodes', 'srcmap',
+        'srcmap-runtime', 'storage-layout', 'userdoc'
+    ]
+    dockerWorkdir = '/casa'
+
+    def __init__(self, w3: web3.Web3, name: str):
+        self.w3 = w3
+        self.name = name
+        self.json = None
+        self.readtextfile()
+        # Did read give us json, or should we compile it?
+        if self.json is None:
+            self.compile()
+            self.writetextfile()
+
+    def dockerargs(self):
+        return [
+            'docker', 'run',
+            '--rm',
+            # Mount our cwd as /casa
+            '-v',
+            '{}:{}'.format(os.getcwd(), self.dockerWorkdir),
+            # Run as us inside the docker, so we have access to this directory
+            '-u', str(os.getuid()),
+            'bfaar/nodo',
+            '/usr/local/bin/solc',
+            '--evm-version', 'byzantium',
+            # Get as many things dumped into our JSON as possible.
+            # You never know when you'll need it, and space is cheap.
+            '--combined-json', ','.join(self.solc_features),
+            # We like optimized things.
+            '--optimize',
+            # File name of input file.
+            '{}/contract.sol'.format(self.dockerWorkdir)
+        ]
+
+    def compile(self):
+        # Make a copy with a fixed name
+        # Mostly so people can use symlinks to the source files
+        # which won't work when we call docker.
+        with open('{}.sol'.format(self.name), 'r') as infile:
+            with open('contract.sol', 'w') as outfile:
+                outfile.write(infile.read())
+        solc = subprocess.run(self.dockerargs(), stdout=subprocess.PIPE, check=True)
+        # Don't leave too much mess.
+        os.remove('contract.sol')
+        txt = solc.stdout
+        output = txt.decode('utf-8')
+        self.json = json.loads(output)
+
+    def readtextfile(self):
+        filename = self.name + '.compiled.json'
+        try:
+            with open(filename, 'rt', encoding='utf-8') as infile:
+                output = infile.read()
+        except FileNotFoundError:
+            return
+        if len(output) < 2:
+            print("The JSON file is too little ({} bytes read from {}).".format(len(output), filename), file=sys.stderr)
+            raise NameError
+        try:
+            self.json = json.loads(output)
+        except json.decoder.JSONDecodeError as exc:
+            print("It was not possible to parse the JSON file (from is {}).".format(filename), file=sys.stderr)
+            raise exc
+
+    def writetextfile(self):
+        filename = self.name + '.compiled.json'
+        try:
+            with open(filename, 'xt', encoding='utf-8') as outfile:
+                outfile.write(json.dumps(self.json))
+        except:
+            os.remove(filename)
+            raise
+
+    def _where(self):
+        # Inch our way closer and closer, all the time,
+        # as long as we see labels which look vaguely familiar.
+        # This may enable us to be more liberal in the JSON input
+        # we receive from the file containing the cached compiled JSON.
+        point = self.json
+        for teststring in [
+            'contracts',
+            '{}/{}.sol:{}'.format(self.dockerWorkdir, 'contract', self.name),
+            '{}/{}.sol:{}'.format(self.dockerWorkdir, self.name, self.name)
+        ]:
+            if type(point) is dict and teststring in point:
+                point = point[teststring]
+        return point
+
+    def bytecode(self):
+        self._where()
+        return '0x' + self._where()['bin']
+
+    def abi(self):
+        # Old method which also works, but our own class has a nicer __str__:
+        # return json.loads(self.json['contracts'][where]['abi'])
+        return Abi(json.loads(self._where()['abi']))
+
+    def instance(self, *args, **kwargs):
+        addr = kwargs.get('address')
+        if addr is None:
+            account = kwargs.get('account')
+            if account is None:
+                raise KeyError('Either address or account must be speficied, in order to get an instance.')
+            ourinstance = self.w3.eth.contract(abi=self.abi(), bytecode=self.bytecode())
+            receipt = account.transact(*args, web3=self.w3, function=ourinstance.constructor)
+            if receipt.status == 0:
+                raise SystemError('Failed to deploy.')
+            addr = receipt.contractAddress
+        return self.w3.eth.contract(address=addr, abi=self.abi())
diff --git a/bin/libbfa/requirements.txt b/bin/libbfa/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..08b1a03bf0a6d5cefc84a40351ed873cffa9e184
--- /dev/null
+++ b/bin/libbfa/requirements.txt
@@ -0,0 +1,4 @@
+web3
+rusty-rlp
+ecdsa
+Crypto
diff --git a/bin/ssn-test.py b/bin/ssn-test.py
new file mode 100755
index 0000000000000000000000000000000000000000..207268069cbe34363fc58adb569b1467a1090a8d
--- /dev/null
+++ b/bin/ssn-test.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+import libbfa
+
+bee = '0xbee0966BdC4568726AB3d3131b02e6255e29285D'
+majoraddr = None
+majoraddr = '0xE676dCaabF935B78beC37730cd5fE243Af1D7329'
+bfa = libbfa.Bfa('test2')
+acct = libbfa.Account(bee, '')
+Majority = libbfa.CompiledContract(bfa.w3, 'Majority')
+majorcontr = Majority.instance(86400 * 7, address=majoraddr, account=acct)
+majoraddr = majorcontr.address
+print("La direccion de Majority es", majoraddr)
+print(majorcontr.functions.councilLength().call())
+
+Cauciones = libbfa.CompiledContract(bfa.w3, 'cauciones')
+cauaddr = None
+cauaddr = "0xcc3D37deD642BB3E7b20d66530667AE7E6CB3750"
+caucontr = Cauciones.instance(majoraddr, account=acct, address=cauaddr)
+cauaddr = caucontr.address
+print("La direccion de cauciones es", cauaddr)