diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ec66ab7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+config.ini
+config.ini.backup*
+run.sh
+admin-cli.js
+logs/
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..822a3f2
--- /dev/null
+++ b/index.js
@@ -0,0 +1,29 @@
+var fs = require('fs');
+var ini = require('ini');
+var expresso = require('sate24-expresso');
+var config = ini.parse(fs.readFileSync(__dirname + '/config.ini', 'utf-8'));
+
+process.chdir(__dirname);
+
+var logDirectory = __dirname + '/logs';
+fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);
+
+var logger = require('sate24/logger.js').start();
+var HttpServer = require('sate24/httpserver.js');
+var aaa = require('sate24/aaa.js');
+var partner = require('./partner-fm.js');
+
+var matrix = aaa.prepareMatrix();
+
+var options = {
+    'aaa': aaa,
+    'logger': logger,
+    'config': config,
+    'matrix': matrix,
+}
+
+var httpServer = HttpServer.start(config, options);
+
+partner.start(config, aaa.callbackReport, options);
+aaa.start(config, partner, options);
+expresso.start(options);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..41c57d5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+  "name": "sate24-to-fm-bp",
+  "version": "1.0.0",
+  "description": "ST24 H2H-OUT to FM based on belanjapulsa.com",
+  "main": "index.js",
+  "scripts": {
+    "test": "mocha"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git@gitlab.kodesumber.com:reload97/sate24-to-fm-bp.git"
+  },
+  "keywords": [
+    "st24",
+    "reload97",
+    "r97",
+    "ppob",
+    "fm",
+    "flashmachine"
+  ],
+  "author": "Adhidarma Hadiwinoto <me@adhisimon.org>",
+  "license": "ISC",
+  "dependencies": {
+    "request": "^2.74.0",
+    "sate24": "git+http://gitlab.kodesumber.com/reload97/node-sate24.git",
+    "sate24-expresso": "git+http://gitlab.kodesumber.com/reload97/sate24-expresso.git",
+    "xml2js": "^0.4.17"
+  },
+  "devDependencies": {
+    "should": "^11.1.0"
+  }
+}
diff --git a/partner-fm.js b/partner-fm.js
new file mode 100644
index 0000000..70792de
--- /dev/null
+++ b/partner-fm.js
@@ -0,0 +1,178 @@
+var xml2js = require('xml2js');
+
+var aaa;
+var _callbackReport;
+var config;
+var logger;
+
+var xmlBuilder = new xml2js.Builder();
+
+function start(options) {
+    if (!options) {
+        console.log('Undefined options, terminating....');
+        process.exit(1);
+    }
+
+    if (options.config) {
+        config = options.config;
+    } else {
+        console.log('Undefined options.config, terminating....')
+        process.exit(1);
+    }
+
+    if (options.aaa) {
+        aaa = options.aaa;
+        _callbackReport = options.aaa.callbackReportWithPushToMongoDb;
+    } else {
+        console.log('Undefined options.aaa, terminating....')
+        process.exit(1);
+    }
+
+    if (options && options.logger) {
+        logger = options.logger;
+    } else {
+        console.log('Undefined options.logger, terminating....')
+        process.exit(1);
+    }
+
+    createServer();
+
+    /*
+    resendDelay.init({
+        config: config,
+        topupRequest: topupRequest,
+        logger: logger
+    });
+    */
+}
+
+function topupRequest(task) {
+    aaa.insertTaskToMongoDb(task);
+
+    var payload = composeTopupStatusMessage(
+        config.h2h_out.pin,
+        task.remoteProduct,
+        task.destination,
+        task.requestId
+    );
+
+    var reqOpts = {
+        url: config.h2h_out.partner_url,
+        method: "POST",
+        headers: {
+            'Content-Type': 'text/xml',
+            'Content-Length': Buffer.byteLength(payload)
+        }
+    }
+
+    logger.verbose('Requesting to partner', {reqOpts: reqOpts, payload: payload});
+    var buffer = "";
+    var req = http.request(reqOpts, function( res ) {
+
+        logger.info('Status code: ' + res.statusCode );
+        var buffer = "";
+        res.on( "data", function( data ) { buffer = buffer + data; } );
+        res.on( "end", function( data ) {
+            logger.verbose('Got a direct response from partner', {response: buffer, task: task});
+            topupResponseHandler(buffer, task.requestId);
+        });
+    });
+}
+
+function topupResponseHandler(xmlResponse, _requestId, cb) {
+    var xmlParser = xml2js.parseString;
+    xmlParser(xmlResponse, function(err, data) {
+        var msg;
+        var requestId;
+        var rc = '68';
+
+        if (_requestId) {
+            requestId = _requestId;
+        }
+
+        if (err) {
+            msg = 'Error parsing xml response: ' + err;
+
+            if (logger) {
+                logger.warn(msg, {err: err, response: xmlResponse, task: task});
+            } else {
+                console.log(msg);
+            }
+        } else {
+
+            try {
+                msg = data.fm.message
+            }
+            catch(e) {
+                msg = 'Unknown message'
+            }
+
+            if (data.fm.status == '0') {
+
+                rc = '00';
+                msg = modifyMessageWithSn(msg);
+
+            } else if (data.fm.status == '1') {
+                rc = '68';
+            } else if (data.fm.status == '2') {
+                rc = '40';
+            } else if (data.fm.status == '3') {
+                rc = '40';
+            } else {
+                rc = '68';
+            }
+
+            if (data.fm.refTrxid) {
+                requestId = data.fm.refTrxid;
+            }
+
+        }
+
+        cb(requestId, rc, msg, xmlResponse)
+    });
+}
+
+function callbackReport(requestId, responseCode, msg, rawResponse) {
+    if (requestId) {
+        _callbackReport(requestId, responseCode, msg, null, rawResponse);
+    } else {
+        logger.warn('Undefined requestId, not sending callbackReport', {rc: responseCode, msg: msg, rawResponse: rawResponse});
+    }
+
+}
+
+function getSnFromMessage(msg) {
+    try {
+        var matches = msg.match(/SN:(\w+)/);
+        return matches[1];
+    }
+    catch(e) {
+        return;
+    }
+}
+
+function modifyMessageWithSn(msg) {
+    var sn = getSnFromMessage(msg);
+    if (sn) {
+        msg = 'SN=' + sn + '; ' + msg;
+    }
+    return msg;
+}
+
+function composeTopupStatusMessage(pin, product, destination, requestId) {
+    var data = {fm: {
+        command: 'TOPUP',
+        pin: pin,
+        product: product,
+        msisdn: destination,
+        refTrxid: requestId
+    }}
+
+    return xmlBuilder.buildObject(data);
+}
+
+exports.start = start;
+exports.topupRequest = topupRequest;
+exports.composeTopupStatusMessage = composeTopupStatusMessage;
+exports.getSnFromMessage = getSnFromMessage;
+exports.modifyMessageWithSn = modifyMessageWithSn;
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..b92976b
--- /dev/null
+++ b/test.js
@@ -0,0 +1,28 @@
+var should = require('should');
+var crypto = require('crypto');
+
+describe('#partner', function () {
+    var partner = require('./partner-fm')
+
+    describe('#composeTopupStatusMessage', function() {
+        it('should return correct xml message', function() {
+            var msg = partner.composeTopupStatusMessage('1234', 'S10', '08120812', '2345');
+            crypto.createHash('sha256').update(msg, 'utf8').digest().toString('hex').should.equal('1b926cb9101d9b172ae12206d0c10d4800b553f3d9f2e320fe526c7effb11985');
+        })
+    });
+
+    describe('#getSnFromMessage', function() {
+        it('should return correct sn', function() {
+            partner.getSnFromMessage('S10.081300000000 berhasil, SN:123456789').should.equal('123456789');
+        });
+    })
+
+    describe('#modifyMessageWithSn', function() {
+        it('should return correct sn', function() {
+            partner.modifyMessageWithSn('S10.081300000000 berhasil.').should.equal('S10.081300000000 berhasil.');
+            partner.modifyMessageWithSn('S10.081300000000 berhasil, SN:').should.equal('S10.081300000000 berhasil, SN:');
+            partner.modifyMessageWithSn('S10.081300000000 berhasil, SN:123456789').should.equal('SN=123456789; S10.081300000000 berhasil, SN:123456789');
+            partner.modifyMessageWithSn('S10.081300000000 berhasil, SN:123456789. Berita tambahan').should.equal('SN=123456789; S10.081300000000 berhasil, SN:123456789. Berita tambahan');
+        });
+    });
+})