Replenishing Consumption Account Balance
Last updated
Was this helpful?
Last updated
Was this helpful?
Was this helpful?
import axios from 'axios';
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
function decodePrivateKey(privateKeyBase64) {
const raw = util.decodeBase64(privateKeyBase64);
if (raw.length === nacl.sign.seedLength) {
return nacl.sign.keyPair.fromSeed(raw).secretKey;
}
if (raw.length === nacl.sign.secretKeyLength) {
return raw;
}
throw new Error('Invalid private key length; expected 32-byte seed or 64-byte secret key');
}
const BASE_URL = 'https://trading.api.thegrid.ai';
const MIN_BALANCE = 5; // Minimum units to maintain
const BUY_QUANTITY = 10;
const MAX_PRICE = 2.50;
const MARKET_ID = 'market_788dcbd5-ac68-4c61-acf1-4443beaf2a1c';
const ORDER_TYPE = (process.env.ORDER_TYPE || 'limit').toLowerCase();
class SignatureAuth {
constructor(privateKeyBase64, fingerprint) {
this.privateKey = decodePrivateKey(privateKeyBase64);
this.fingerprint = fingerprint;
}
getHeaders(method, path, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const message = `${timestamp}${method.toUpperCase()}${path}${body}`;
const messageBytes = util.decodeUTF8(message);
const signatureBytes = nacl.sign.detached(messageBytes, this.privateKey);
return {
'x-thegrid-signature': util.encodeBase64(signatureBytes),
'x-thegrid-timestamp': timestamp,
'x-thegrid-fingerprint': this.fingerprint
};
}
}
const auth = new SignatureAuth(
process.env.GRID_TRADING_PRIVATE_KEY,
process.env.GRID_TRADING_FINGERPRINT
);
function buildOrderPayload(orderType) {
const type = (orderType || 'limit').toLowerCase();
if (type === 'limit') {
return {
market_id: MARKET_ID,
side: 'buy',
type: 'limit',
quantity: BUY_QUANTITY,
price: MAX_PRICE.toString(),
time_in_force: 'gtc',
client_order_id: `replenish-${Date.now()}`
};
}
if (type === 'market') {
return {
market_id: MARKET_ID,
side: 'buy',
type: 'market',
quantity: BUY_QUANTITY,
time_in_force: 'gtc',
client_order_id: `replenish-${Date.now()}`
};
}
throw new Error('ORDER_TYPE must be "limit" or "market"');
}
async function submitOrder(orderType) {
const orderPath = '/api/v1/trading/orders';
const orderData = buildOrderPayload(orderType);
const body = JSON.stringify(orderData);
const orderResp = await axios.post(`${BASE_URL}${orderPath}`, body, {
headers: {
'Content-Type': 'application/json',
...auth.getHeaders('POST', orderPath, body)
}
});
return orderResp.data.data.order_id;
}
// Check current consumption balance
const balancePath = '/api/v1/trading/consumption-accounts';
const balanceResp = await axios.get(`${BASE_URL}${balancePath}?order_by=created_at`, {
headers: auth.getHeaders('GET', balancePath)
});
const account = balanceResp.data.data[0];
const currentBalance = account?.available_balance || 0;
console.log(`Current balance: ${currentBalance} units`);
if (currentBalance >= MIN_BALANCE) {
console.log(`Balance is sufficient (>= ${MIN_BALANCE}), no replenishment needed`);
} else {
console.log(`Balance is low (< ${MIN_BALANCE}), placing ${ORDER_TYPE} buy order...`);
const orderId = await submitOrder(ORDER_TYPE);
console.log(`Order placed (${ORDER_TYPE}): ${orderId}`);
}
package main
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const (
baseURL = "https://trading.api.thegrid.ai"
minBalance = 5
buyQuantity = 10
maxPrice = 2.50
marketID = "market_788dcbd5-ac68-4c61-acf1-4443beaf2a1c"
)
type SignatureAuth struct {
privateKey ed25519.PrivateKey
fingerprint string
}
func buildOrderPayload(orderType string) (map[string]any, error) {
orderType = strings.ToLower(orderType)
if orderType == "" {
orderType = "limit"
}
orderData := map[string]any{
"market_id": marketID,
"side": "buy",
"type": orderType,
"quantity": buyQuantity,
"time_in_force": "gtc",
"client_order_id": fmt.Sprintf("replenish-%d", time.Now().Unix()),
}
switch orderType {
case "limit":
orderData["price"] = fmt.Sprintf("%.2f", maxPrice)
case "market":
// Market orders execute at the best available price, no price required.
default:
return nil, fmt.Errorf("unsupported ORDER_TYPE %q (use limit or market)", orderType)
}
return orderData, nil
}
func NewSignatureAuth(privateKeyB64, publicKeyB64 string) *SignatureAuth {
privateKeyBytes, _ := base64.StdEncoding.DecodeString(privateKeyB64)
var privateKey ed25519.PrivateKey
if len(privateKeyBytes) == ed25519.SeedSize {
privateKey = ed25519.NewKeyFromSeed(privateKeyBytes)
} else {
privateKey = ed25519.PrivateKey(privateKeyBytes)
}
publicKeyBytes, _ := base64.StdEncoding.DecodeString(publicKeyB64)
hash := sha256.Sum256(publicKeyBytes)
fingerprint := strings.TrimRight(base64.StdEncoding.EncodeToString(hash[:]), "=")
return &SignatureAuth{privateKey: privateKey, fingerprint: fingerprint}
}
func (sa *SignatureAuth) GetHeaders(method, path, body string) map[string]string {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
message := timestamp + method + path + body
signature := ed25519.Sign(sa.privateKey, []byte(message))
return map[string]string{
"x-thegrid-signature": base64.StdEncoding.EncodeToString(signature),
"x-thegrid-timestamp": timestamp,
"x-thegrid-fingerprint": sa.fingerprint,
}
}
func main() {
auth := NewSignatureAuth(
os.Getenv("GRID_TRADING_PRIVATE_KEY"),
os.Getenv("GRID_TRADING_PUBLIC_KEY"),
)
// Check current consumption balance
balancePath := "/api/v1/trading/consumption-accounts"
req, err := http.NewRequest("GET", baseURL+balancePath+"?order_by=created_at", nil)
if err != nil {
log.Fatal(err)
}
for k, v := range auth.GetHeaders("GET", balancePath, "") {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var balanceResult struct {
Data []struct {
AvailableBalance float64 `json:"available_balance"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&balanceResult); err != nil {
log.Fatal(err)
}
currentBalance := 0.0
if len(balanceResult.Data) > 0 {
currentBalance = balanceResult.Data[0].AvailableBalance
}
fmt.Printf("Current balance: %.0f units\n", currentBalance)
if currentBalance >= minBalance {
fmt.Printf("Balance is sufficient (>= %d), no replenishment needed\n", minBalance)
return
}
fmt.Printf("Balance is low (< %d), placing buy order...\n", minBalance)
orderType := strings.ToLower(os.Getenv("ORDER_TYPE"))
orderData, err := buildOrderPayload(orderType)
if err != nil {
log.Fatal(err)
}
orderJSON, err := json.Marshal(orderData)
if err != nil {
log.Fatal(err)
}
orderPath := "/api/v1/trading/orders"
orderReq, err := http.NewRequest("POST", baseURL+orderPath, bytes.NewBuffer(orderJSON))
if err != nil {
log.Fatal(err)
}
orderReq.Header.Set("Content-Type", "application/json")
for k, v := range auth.GetHeaders("POST", orderPath, string(orderJSON)) {
orderReq.Header.Set(k, v)
}
orderResp, err := http.DefaultClient.Do(orderReq)
if err != nil {
log.Fatal(err)
}
defer orderResp.Body.Close()
var orderResult struct {
Data struct {
OrderID string `json:"order_id"`
} `json:"data"`
}
if err := json.NewDecoder(orderResp.Body).Decode(&orderResult); err != nil {
log.Fatal(err)
}
fmt.Printf("Order placed (%s): %s\n", orderData["type"], orderResult.Data.OrderID)
}