Constructing a transaction

Build, preview, and send — no BigInt.

A NEAR transaction is a receiverId plus an ordered list of actions. With @fastnear/api you declare the actions, preview them, and sign from a wallet or a local key. There is one rule to learn, and it removes the whole class of big-number snags: you pass amounts in whatever form is handy, and you always get decimal strings back.

The one rule
  • In: gas, deposits, and amounts accept a string, number, or bigint — plus readable unit strings like "100 Tgas" and "0.01 NEAR". Unit strings are resolved by the local signing path; going through a wallet, convert them yourself with near.utils.convertUnit.
  • Out: what @fastnear decodes — borsh deserialize, near.ft.*, near.utils.txToJson — gives wide integers back as decimal strings. Results that pass RPC JSON straight through keep NEAR's own types, so yocto fields (deposit, tokens_burnt) are strings while gas_burnt and access-key nonce are JSON numbers.
  • Inspect a transaction with near.explain.tx(...) or near.utils.txToJson(...) — never JSON.stringify a raw bigint (it throws).
  • You never need BigInt to build, send, or read a transaction.
Declare + preview

near.explain.tx is a pure function — it returns a JSON-safe summary of exactly what you are about to sign, with no network call and no wallet popup.

// One action: draw a green pixel on berryclub.ek.near.
const actions = [
  near.actions.functionCall({
    methodName: "draw",
    args: { pixels: [{ x: 10, y: 20, color: 65280 }] }, // color is u32, not a hex string
    gas: "100 Tgas", // or 100000000000000, or 100000000000000n
    deposit: "0",    // draw is not payable — see "Buy tokens" below for a deposit
  }),
];

// Preview before signing — JSON-safe, gas/deposit stay as you wrote them.
near.print(near.explain.tx({
  signerId: "alice.near",
  receiverId: "berryclub.ek.near",
  actions,
}));
Sign and send

The same actions, two signers.

Once the actions are declared, sending is one call. Either a wallet holds the key — the usual browser case — or you sign locally with a full-access key, which is the usual server case but is not limited to Node. Both accept the identical action shape, but only the local path resolves unit strings: a wallet forwards gas and deposit verbatim to its executor, which calls BigInt() on them. Pass anything readable through near.utils.convertUnit first.

Browser wallet

After nearWallet.connect(...), the wallet signs. Load the IIFE globals (near.js, wallet.js) or import @fastnear/api + @fastnear/wallet.

// Sign in first: this is what puts the account in near.state, and
// sendTx reads that — not the wallet — to decide who signs.
await near.recipes.connect({ contractId: "berryclub.ek.near" });

// Resolve unit strings BEFORE they cross into a wallet: the wallet
// forwards gas/deposit verbatim to the executor, which calls BigInt().
const result = await near.recipes.functionCall({
  receiverId: "berryclub.ek.near",
  methodName: "draw",
  args: { pixels: [{ x: 10, y: 20, color: 65280 }] },
  gas: near.utils.convertUnit("100 Tgas"),
  deposit: "0",
});

// Yocto fields (deposit, tokens_burnt) are decimal strings;
// gas_burnt comes straight from the RPC as a JSON number.
near.print(result);
Node, local key

Sign with a full-access key from @fastnear/utils. Keep the key in server-side secret storage — never ship it to a browser. Passing signer explicitly works on any version; to skip it on every call, persist the key once with near.state.updateAccountState and sendTx will find it (@fastnear/api 2.1.1+ — see keys & accounts).

import { actions, config, sendTx } from "@fastnear/api";
import { signerFromPrivateKey } from "@fastnear/utils";

config({ networkId: "mainnet" });

const result = await sendTx({
  signerId: "alice.near",
  signer: signerFromPrivateKey(privateKey), // full-access key
  receiverId: "berryclub.ek.near",
  actions: [
    actions.functionCall({
      methodName: "draw",
      args: { pixels: [{ x: 10, y: 20, color: 65280 }] },
      gas: "100 Tgas",
      deposit: "0",
    }),
  ],
  waitUntil: "FINAL",
});
Attaching a deposit

draw above is not payable, so its deposit is "0". When a method does take NEAR, write the amount the way you say it — buy_tokens sells 250 🥑 per NEAR, so this buys 2.5.

// Readable units in; the wallet popup confirms the exact deposit.
await near.recipes.connect({ contractId: "berryclub.ek.near" });

const result = await near.recipes.functionCall({
  receiverId: "berryclub.ek.near",
  methodName: "buy_tokens",
  args: {},
  gas: near.utils.convertUnit("100 Tgas"),
  // convertUnit also takes a yocto string like "10000000000000000000000"
  deposit: near.utils.convertUnit("0.01 NEAR"),
});

near.print(result);
Wide integers

Everything we decode is a string.

Yocto amounts exceed Number's safe range, so @fastnear never decodes them to plain JS numbers — everything it deserializes comes back as a decimal string, so a decoded transaction survives JSON.stringify and re-encodes to identical bytes. Values it merely relays from the RPC keep whatever type NEAR sent. Reach for BigInt only for arithmetic.

What this buys you
  • JSON.stringify(tx) never throws — there is no bigint to trip on.
  • Values from near.ft.balance and decoded borsh are strings, so they interoperate without conversion. near.view is a pass-through — it returns the contract's own JSON, so a field like ft_metadata().decimals is a number because the contract said so.
  • Serializing accepts string | number | bigint, so a value you read can be sent straight back.
Decoding borsh yourself?
  • @fastnear/borsh deserialize returns u64/u128 as decimal strings by default.
  • Pass deserialize(schema, bytes, { bigints: "bigint" }) to opt into native bigint when you want arithmetic.
  • u8/u16/u32 stay JS numbers (they fit safely).
Go deeper

AI agents: the "Result shapes" section of llms-full.txt states the wide-integers rule, and recipes.json is the structured task catalog (see the explain-transaction, function-call, and transfer recipes). Rate-limited? Free trial credits are at dashboard.fastnear.com — set near.config({ apiKey }) once you have one.