Making Blockchain Physical
Tap your NFC chip to execute any blockchain transactionโno wallet popups, no gas fees, no friction
Live Demo โข Documentation โข Contracts
- 8 Smart Contracts deployed across 2 testnets
- 5 Core Contracts for universal execution
- 3 Advanced Extensions (Bridge, Aave Flash Loans)
- Sub-3-second tap-to-pay experience
- $0 gas fees for users (relay-sponsored)
- 100% gasless UX for end users
Current blockchain UX creates insurmountable friction:
Traditional Web3 Transaction:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Open wallet app (5s) โ
โ 2. Switch to correct network (10s) โ
โ 3. Approve token spending (15s) โ
โ 4. Confirm transaction (10s) โ
โ 5. Wait for confirmation (30s) โ
โ 6. Check if it worked (5s) โ
โ โ
โ Total: 75 seconds, 6 steps, 4 popups โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Result: Users abandon transactions, developers compromise security for UX, mainstream adoption stalls.
Physical tap authorization replaces the entire flow:
TapThat X Transaction:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Tap phone on NFC chip (2s) โ
โ 2. Transaction executes (1s) โ
โ โ
โ Total: 3 seconds, 1 step, 0 popups โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- One-time setup: Register NFC chip + pre-approve spending limits (5 minutes)
- Every transaction: Tap phone โ chip signs authorization โ relay executes (3 seconds)
- Result: Physical blockchain interactions with zero ongoing friction
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TapThat X Protocol โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโ
โ NFC Chip (HaLo) โ โ Secure element with private key
โ Signs EIP-712 msgs โ Keys never leave chip
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ tap (Web NFC API)
โ 2-3 seconds
โโโโโโโโโโโโผโโโโโโโโโโโ
โ Mobile Device โ โ @arx-research/libhalo
โ (Android/iOS) โ Browser-based NFC reading
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ HTTPS POST
โ /api/relay-execute-tap
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Gasless Relay (Next.js API) โ โ Backend relayer
โ - Validates request structure โ Pays gas for users
โ - Detects operation type โ Signs with private key
โ - Sets dynamic gas limits โ
โ - Submits transaction on-chain โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ writeContract()
โ TapThatXExecutor.executeTap()
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TapThatXExecutor (Orchestrator) โ
โ 1. Fetch configuration from storage โ
โ 2. Validate config active โ
โ 3. Delegate to Protocol for execution โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโผโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ
โ TapThatXConfiguration โ โ TapThatXProtocol โ
โ (Action Storage) โ โ (Validation & Execution Engine) โ
โ โ โ โ
โ getConfiguration() โ โ executeAuthorizedCall() โ
โ โ ActionConfig { โ โ โโ Verify nonce not used โ
โ target, โ โ โโ Recover chip from signature โ
โ callData, โ โ โโ Validate timestamp <5 min โ
โ description โ โ โโ Check chip ownership โ
โ } โ โ โโ Mark nonce used โ
โ โ โ โโ Execute target.call() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโดโโโโโโโโโโ
โ โ
โโโโโโโโโโผโโโโโโโโโโโ โโโโโโโผโโโโโโโโโโโโโโโ
โ TapThatXRegistry โ โ Target Contract โ
โ (Chip Ownership) โ โ (Action Execute) โ
โ โ โ โ
โ hasChip() โ โ โข ERC20 transfer โ
โ โ validates owner โ โ โข Bridge ETH โ
โ owns chip โ โ โข Aave rebalance โ
โโโโโโโโโโโโโโโโโโโโโ โ โข Uniswap swap โ
โ โข Custom call โ
โโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Contract Architecture โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ TapThatXRegistry โ โ No dependencies
โ (Chip โ Owner map) โ Foundation layer
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ depends on
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโผโโโโโโโโโโโ โโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ TapThatXProtocol โ โ TapThatXConfiguration โ
โ (Core execution) โ โ (Action storage) โ
โโโโโโโโโโโฌโโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ both required
โโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ TapThatXExecutor โ
โ (Simplified interface) โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ required by extensions
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโโโโผโโโโโโโ โโโโโโโผโโโโโโโ โโโโโโผโโโโโโโโโโโโโโโโโโ
โ TapThatXBridge โ โ TapThatX โ โ TapThatXAave โ
โ ETHViaWETH โ โ Aave โ โ PositionCloser โ
โ (Dual L2 bridge) โ โ Rebalancer โ โ (Flash loan close) โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
Extension Extension Extension
(Sepolia only) (Base only) (Base only)
Purpose: Chip ownership registry with EIP-712 proof of possession
Deployed At:
- Base Sepolia:
0x91D05d5B8913BCdA59f1923dC6831B108154Df22 - Sepolia:
0x...(see deployedContracts.ts)
Key Data Structures:
// Bidirectional mappings for O(1) lookups
mapping(address => address[]) private ownerToChips;
mapping(address => address[]) private chipToOwners;
mapping(address => mapping(address => bool)) public ownerHasChip;Key Functions:
function registerChip(address chipAddress, bytes memory chipSignature) external- Purpose: Register NFC chip to owner with cryptographic proof
- Validation: Verifies chip signed EIP-712
ChipRegistrationmessage - Security: Uses chain-agnostic domain separator (no
chainId) - Gas: ~150k
function getOwnerChips(address owner) external view returns (address[] memory)- Purpose: Get all chips registered to an owner
- Use case: Display user's registered chips in UI
function hasChip(address owner, address chip) external view returns (bool)- Purpose: Fast ownership validation
- Use case: Called by Protocol before every execution
EIP-712 Domain (Chain-Agnostic):
{
name: "TapThatXRegistry",
version: "1",
verifyingContract: address(this)
// Deliberately excludes chainId for cross-chain chip reuse
}Innovation: Chips registered once work across all EVM chains where protocol is deployed.
Purpose: Core execution engine for chip-authorized contract calls
Deployed At:
- Base Sepolia:
0x0F917750db157D65c6c14e5Ce5828a250569afE1
Security Features:
- โ
Nonce-based replay protection (
mapping(bytes32 => bool) usedNonces) - โ Timestamp validation (5-minute expiration window)
- โ Chip ownership verification (via Registry)
- โ ReentrancyGuard protection
- โ EIP-712 signature validation
Key Function:
function executeAuthorizedCall(
address owner, // Chip owner (transaction originator)
address target, // Contract to call (e.g., USDC, Aave)
bytes calldata callData, // Encoded function call
uint256 value, // ETH value (0 for most ops)
bytes memory chipSignature, // Chip's EIP-712 signature
uint256 timestamp, // Authorization timestamp
bytes32 nonce // Unique nonce (prevents replay)
) external payable nonReentrant returns (bool success, bytes memory returnData)Execution Flow:
1. Validate nonce unused require(!usedNonces[nonce])
2. Recover chip from signature TapThatXAuth.recoverChipFromCallAuth()
3. Validate timestamp fresh require(block.timestamp - timestamp <= 300)
4. Verify chip ownership require(registry.hasChip(owner, chip))
5. Mark nonce used usedNonces[nonce] = true
6. Execute call target.call{value}(callData)
7. Emit event AuthorizedCallExecuted(...)
CallAuthorization Struct (EIP-712):
struct CallAuthorization {
address owner; // Chip owner
address target; // Contract to call
bytes callData; // Function call data
uint256 value; // ETH value
uint256 timestamp; // Authorization time
bytes32 nonce; // Unique nonce
}Gas Cost: 130-150k (simple transfers) to 3M (complex DeFi)
Purpose: On-chain storage for pre-configured chip actions
Deployed At:
- Base Sepolia:
0x...(see deployedContracts.ts)
Data Structure:
struct ActionConfig {
address targetContract; // e.g., 0xUSDC
bytes staticCallData; // Pre-encoded function call
uint256 value; // ETH to send (0 for ERC20)
string description; // "Send 100 USDC to Alice"
bool isActive; // Enable/disable toggle
}
mapping(address => mapping(address => ActionConfig)) public configurations;
// Structure: owner โ chip โ ActionConfigKey Functions:
function setConfiguration(
address chip,
address targetContract,
bytes calldata staticCallData,
uint256 value,
string calldata description
) external- Access Control: Only chip owner can configure
- Validation:
require(registry.hasChip(msg.sender, chip)) - Storage: Saves to
configurations[msg.sender][chip]
function getConfiguration(address owner, address chip)
external view returns (ActionConfig memory)- Purpose: Fetch pre-configured action
- Used by: TapThatXExecutor before every execution
function toggleConfiguration(address chip) external- Purpose: Enable/disable without deleting configuration
- Use case: Pause payments while keeping configuration
Purpose: Simplified interface that combines configuration fetch + execution
Deployed At:
- Base Sepolia:
0x...
Key Function:
function executeTap(
address owner,
address chip,
bytes memory chipSignature,
uint256 timestamp,
bytes32 nonce
) external payable nonReentrant returns (bool, bytes memory)Execution Steps:
// 1. Fetch configuration
ActionConfig memory config = configuration.getConfiguration(owner, chip);
// 2. Validate configuration
require(config.targetContract != address(0), "No configuration");
require(config.isActive, "Configuration inactive");
// 3. Execute via protocol
(bool success, bytes memory data) = protocol.executeAuthorizedCall{value: config.value}(
owner,
config.targetContract,
config.staticCallData,
config.value,
chipSignature,
timestamp,
nonce
);
// 4. Emit event
emit TapExecuted(owner, chip, config.targetContract, nonce, success, config.description);Why Use Executor?
- Simpler: Single function call instead of fetching config separately
- Safer: Validates config exists and is active
- Gas-efficient: Optimized for relay usage
Purpose: Signature verification utilities for EIP-712
Key Functions:
function recoverChipFromCallAuth(
bytes32 domainSeparator,
CallAuthorization memory auth,
bytes memory signature
) internal pure returns (address chipAddress)Implementation:
// 1. Build struct hash
bytes32 structHash = keccak256(abi.encode(
CALL_AUTH_TYPEHASH,
auth.owner,
auth.target,
keccak256(auth.callData), // Hash callData
auth.value,
auth.timestamp,
auth.nonce
));
// 2. Build EIP-712 digest
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
domainSeparator,
structHash
));
// 3. Recover signer
return digest.recover(signature);function validateTimestamp(uint256 timestamp, uint256 maxWindow)
internal view returns (bool)- Validation:
timestamp <= block.timestamp && (block.timestamp - timestamp) <= maxWindow - Window: 300 seconds (5 minutes)
Purpose: Gasless cross-chain ETH bridging to Base + Optimism Sepolia
Deployed At:
- Sepolia:
0x...(L1 only)
Why This Extension?
Problem: Users need ETH on L2s but lack gas to bridge. Solution: Use WETH (ERC20) approval โ unwrap โ bridge atomically.
Key Function:
function unwrapAndBridgeDual(
address owner,
uint32 minGasLimitOP,
uint32 minGasLimitBase
) externalExecution Flow:
1. Pull pre-approved WETH weth.transferFrom(owner, this, amount)
2. Unwrap WETH โ ETH weth.withdraw(amount)
3. Split amount for 2 chains toBase = amount / 2; toOP = amount - toBase
4. Bridge to Base Sepolia bridgeBase.depositETHTo{value: toBase}(...)
5. Bridge to OP Sepolia bridgeOP.depositETHTo{value: toOP}(...)
6. Emit event ETHBridgedViaWETH(...)
Gas Requirements: 3M gas
- OP bridge: ~913k
- Base bridge: ~650k
- Cross-chain messaging: ~200k
- WETH operations: ~150k
- Safety buffer: ~1.1M
Network Addresses (Sepolia):
WETH = 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9
BRIDGE_OP = 0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1 // L1StandardBridge
BRIDGE_BASE = 0xfd0Bf71F60660E2f608ed56e1659C450eB113120 // L1StandardBridgeUser Flow:
Setup (once):
1. Approve WETH to bridge extension
Execute (anytime):
1. Tap chip
2. Extension pulls WETH, unwraps, bridges
3. User receives ETH on both L2s
Purpose: Atomically rebalance Aave positions using flash loans
Deployed At:
- Base Sepolia:
0x...
The Impossible Problem:
To rebalance an Aave position (reduce debt, improve health factor), users need:
- Option 1: Deposit more collateral (requires capital)
- Option 2: Repay debt (requires debt tokens)
- Option 3: Flash loan + complex execution (requires technical knowledge)
TapThat X Solution: One tap executes flash loan rebalancing.
Optimal Flash Loan Formula:
Health Factor: HF = (totalCollateral ร liquidationThreshold) / totalDebt
Target: targetHF = (totalCollateral ร LT) / (totalDebt - debtRepaid)
Solving for debtRepaid:
debtRepaid = (targetHF ร totalDebt - totalCollateral ร LT) / (targetHF - costFactor ร LT)
Where:
costFactor = 1.0000 + 0.0009 (flash fee) + 0.0030 (margin) + maxSlippage
Implementation:
function calculateOptimalFlashLoan(address owner, RebalanceConfig memory config)
public view returns (uint256 flashLoanAmount)
{
// Get Aave position data (USD, 8 decimals)
(uint256 collateralUSD, uint256 debtUSD, , uint256 ltBps, , uint256 hf) =
POOL.getUserAccountData(owner);
if (hf >= config.targetHealthFactor) return 0;
// Normalize to 18 decimals for precision
uint256 collateral18 = collateralUSD * 1e10;
uint256 debt18 = debtUSD * 1e10;
uint256 lt18 = (ltBps * 1e18) / 10000;
// Cost factor (basis points โ 18 decimals)
// 10000 + 9 (flash) + 30 (margin) + slippage
uint256 costFactorBps = 10000 + 9 + 30 + config.maxSlippage;
uint256 costFactor18 = (costFactorBps * 1e18) / 10000;
// Formula application
uint256 numerator = (config.targetHealthFactor * debt18 / 1e18) -
(collateral18 * lt18 / 1e18);
uint256 denominator = config.targetHealthFactor - (costFactor18 * lt18 / 1e18);
uint256 debtRepaidUSD = (numerator * 1e18 / denominator) / 1e10; // Back to 8 decimals
// Convert USD โ token amount via Aave oracle
address oracle = IPoolAddressesProvider(POOL.ADDRESSES_PROVIDER()).getPriceOracle();
uint256 price = IAaveOracle(oracle).getAssetPrice(config.debtAsset);
uint256 decimals = IERC20Metadata(config.debtAsset).decimals();
return (debtRepaidUSD * (10 ** decimals)) / price;
}Flash Loan Callback Execution (All Atomic):
function executeOperation(...) external returns (bool) {
// Step 1: Repay debt with flash loan
IERC20(debtAsset).approve(address(POOL), flashAmount);
POOL.repay(debtAsset, flashAmount, 2, owner); // Variable rate
// Step 2: Withdraw collateral (aToken)
address aToken = POOL.getReserveData(collateralAsset).aTokenAddress;
IERC20(aToken).transferFrom(owner, address(this), collateralAmount);
POOL.withdraw(collateralAsset, collateralAmount, address(this));
// Step 3: Swap collateral โ debt asset (Uniswap V2)
uint256 totalRepayment = flashAmount + premium;
uint256 swapOutput = _swapV2(collateralAsset, debtAsset, collateralAmount, totalRepayment);
// Step 4: Approve Aave to pull repayment
IERC20(debtAsset).approve(address(POOL), totalRepayment);
// Step 5: Return excess to user
uint256 excess = swapOutput - totalRepayment;
if (excess > 0) IERC20(debtAsset).transfer(owner, excess);
// Step 6: Verify health factor improved
(, , , , , uint256 hfAfter) = POOL.getUserAccountData(owner);
require(hfAfter > hfBefore, "Health factor not improved");
return true; // Aave auto-pulls totalRepayment
}Uniswap V2 Calculation:
// Constant product formula: x ร y = k
// Given output (totalRepayment), calculate input needed:
// amountIn = (reserveIn ร amountOut ร 1000) / ((reserveOut - amountOut) ร 997)
uint256 numerator = uint256(reserveCollateral) * totalRepayment * 1000;
uint256 denominator = (uint256(reserveDebt) - totalRepayment) * 997; // 0.3% fee
uint256 amountIn = (numerator / denominator) + 1;
// Add slippage buffer
uint256 slippageMultiplier = 10000 + config.maxSlippage;
return (amountIn * slippageMultiplier) / 10000;Gas Requirements: 1.5M
- Flash loan callback: 200k
- Aave operations: 350k
- Uniswap swap: 300k
- Verifications: 200k
- Buffer: 450k
Example: Health factor 1.05 โ 2.5 in one tap
Purpose: Fully close Aave positions (repay all debt, withdraw all collateral)
Key Difference from Rebalancer:
- Rebalancer: Reduces debt to improve health factor
- Closer: Repays ALL debt and withdraws ALL collateral
Execution Flow:
1. Calculate total debt flashLoanAmount = total variable debt
2. Flash loan exact amount POOL.flashLoanSimple(debtAsset, amount, ...)
3. Repay ALL debt POOL.repay(debtAsset, amount, 2, owner)
4. Withdraw ALL collateral POOL.withdraw(collateralAsset, aTokenBalance, ...)
5. Swap collateral โ debt swapOutput = _swapV2(collateral, debt, ...)
6. Repay flash loan IERC20(debt).approve(POOL, repayment)
7. Return excess to user transfer(owner, remainingCollateral + excessDebt)
8. Verify fully closed require(remainingDebt == 0)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Chip Registration โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
User Frontend NFC Chip Blockchain
โ โ โ โ
โ Navigate to โ โ โ
โ /register โ โ โ
โโโโโโโโโโโโโโโโโโโโโ>โ โ โ
โ โ โ โ
โ Click "Register" โ โ โ
โโโโโโโโโโโโโโโโโโโโโ>โ โ โ
โ โ โ โ
โ โ Step 1: Detect Chip โ โ
โ โ "Hold device near โ โ
โ โ NFC chip..." โ โ
โ โ โ โ
โ Tap phone on chip โ โ โ
โโโโโโโโโโโโโโโโโโโโโ>โโ signMessage("init") โ> โ
โ โ โ โ
โ โ<โ {address, sig} โโโโโโค โ
โ โ โ โ
โ โ chipAddress detected โ โ
โ โ 0x742d... โ โ
โ โ โ โ
โ โ Step 2: Sign Auth โ โ
โ โ "Tap again to โ โ
โ โ authorize..." โ โ
โ โ โ โ
โ Tap chip again โ โ โ
โโโโโโโโโโโโโโโโโโโโโ>โโ signTypedData({ โ โ
โ โ domain: { โ โ
โ โ name: "TapThatX โ โ
โ โ Registry",โ โ
โ โ version: "1" โ โ
โ โ }, โ โ
โ โ message: { โ โ
โ โ owner: 0xUser, โ โ
โ โ chip: 0x742d โ โ
โ โ } โ โ
โ โ }) โโโโโโโโโโโโโโโโโโ> โ
โ โ โ โ
โ โ โ Chip signs EIP-712 โ
โ โ โ with private key โ
โ โ โ โ
โ โ<โ chipSignature โโโโโโโค โ
โ โ โ โ
โ โ Step 3: Submit Tx โ โ
โ โ "Confirm in wallet" โ โ
โ โ โ โ
โ Approve in wallet โ โ โ
โโโโโโโโโโโโโโโโโโโโโ>โโ writeContract({ โ โ
โ โ fn: "registerChip",โ โ
โ โ args: [ โ โ
โ โ 0x742d, โ โ
โ โ signature โ โ
โ โ ] โ โ
โ โ }) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ โ โ
โ โ โ TapThatXRegistry โ
โ โ โ 1. Recover signer โ
โ โ โ 2. Verify == chip โ
โ โ โ 3. Store mapping โ
โ โ โ ownerโchips[] โ
โ โ โ chipโowners[] โ
โ โ โ โ
โ โ<โโโโโโ tx hash โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ โ
โ โ
Success! โ โ โ
โ "Chip registered" โ โ โ
โ<โโโโโโโโโโโโโโโโโโโโโค โ โ
โ โ โ โ
Total Time: ~30 seconds
User Actions: 3 taps, 1 wallet approval
Result: Chip โ Owner mapping stored on-chain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Tap-to-Execute Flow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
User Frontend NFC Chip Relay API Blockchain
โ โ โ โ โ
โ Tap phone โ โ โ โ
โ on chip โ โ โ โ
โโโโโโโโโโโโโโ>โ โ โ โ
โ โ Step 1: โ โ โ
โ โ Detect chip โ โ โ
โ โโ signMessage โ> โ โ
โ โ ("init") โ โ โ
โ โ<โ {address} โโโค โ โ
โ โ โ โ โ
โ โ Verify chip โ โ โ
โ โ ownership โ โ โ
โ โโ registry.hasChip() โโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ<โ true โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ โ โ
โ โ Fetch config โ โ โ
โ โโ config.getConfiguration() โโโโโโโโโโโโโโโโโโ>โ
โ โ<โ ActionConfig { โ โ
โ โ target: USDC, โ โ
โ โ callData: transferFrom() โ โ
โ โ } โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ โ โ
โ โ Preview: โ โ โ
โ โ "Send 10 USDC โ โ โ
โ โ to Alice" โ โ โ
โ โ โ โ โ
โ โ Step 2: โ โ โ
โ โ Authorize โ โ โ
โ Tap again โ โ โ โ
โโโโโโโโโโโโโโ>โโ signTypedData({ โ โ
โ โ CallAuth: {โ โ โ
โ โ owner, โ โ โ
โ โ target, โ โ โ
โ โ callData,โ โ โ
โ โ timestamp, โ โ
โ โ nonce โ โ โ
โ โ } โ โ โ
โ โ }) โโโโโโโโโโ> โ โ
โ โ<โ signature โโโค โ โ
โ โ โ โ โ
โ โ Step 3: โ โ โ
โ โ Send to relay โ โ โ
โ โโ POST /api/relay-execute-tap โ> โ
โ โ { โ โ โ
โ โ owner, โ โ โ
โ โ chip, โ โ โ
โ โ signature,โ โ โ
โ โ timestamp,โ โ โ
โ โ nonce โ โ โ
โ โ } โ โ โ
โ โ โ โ โ
โ โ โ โ Relay validates โ
โ โ โ โ input structure โ
โ โ โ โ โ
โ โ โ โ Detect operationโ
โ โ โ โ (simple/Aave/ โ
โ โ โ โ bridge) โ
โ โ โ โ โ
โ โ โ โ Set gas limit: โ
โ โ โ โ - Simple: auto โ
โ โ โ โ - Aave: 1.5M โ
โ โ โ โ - Bridge: 3M โ
โ โ โ โ โ
โ โ โ โ Submit tx โ
โ โ โ โโ executeTap() โโ>
โ โ โ โ โ
โ โ โ โ Executor: โ
โ โ โ โ 1. Fetch config
โ โ โ โ 2. Validate โ
โ โ โ โ โ
โ โ โ โ Protocol: โ
โ โ โ โ 3. Nonce checkโ
โ โ โ โ 4. Recover chip
โ โ โ โ 5. Timestamp โ
โ โ โ โ 6. Ownership โ
โ โ โ โ 7. Execute โ
โ โ โ โ target.callโ
โ โ โ โ โ
โ โ โ โ<โ success โโโโโโโค
โ โ โ โ โ
โ โ<โโโโโโ {success, txHash} โโโโโค โ
โ โ โ โ โ
โ โ
Success! โ โ โ โ
โ "10 USDC sent โ โ โ โ
โ to Alice" โ โ โ โ
โ<โโโโโโโโโโโโโโโค โ โ โ
Total Time: 3 seconds
User Actions: 2 taps
Gas Paid By: Relay
User Gas Cost: $0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Aave Position Rebalancing (Flash Loan) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Scenario: User's health factor is 1.05 (5% from liquidation)
Goal: Improve to 2.5 (safe zone)
Tap โ Relay โ TapThatXExecutor โ TapThatXAaveRebalancer
โ
โ calculateOptimalFlashLoan()
โ Returns: 1000 USDT needed
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Aave V3 Pool โ
โ flashLoanSimple() โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
Flash 1000 USDT (0.09% fee = 0.9 USDT)
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ executeOperation() Callback โ
โ (All steps atomic) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ Step 1: โ โ Step 2: โ โ Step 3: โ
โ Repay Debt โ โ Withdraw โ โ Swap โ
โ โ โ Collateral โ โ โ
โ POOL.repay( โ โ โ โ Uniswap V2 โ
โ USDT, โ โ 1. transferFrom โ โ โ
โ 1000, โ โ user's aToken โ โ collateral โ
โ owner โ โ 2. POOL.withdraw โ โ โ โ
โ ) โ โ collateral โ โ USDT โ
โ โ โ โ โ โ
โ Health: 1.05 โ โ Health: improved โ โ Output: โ
โ โ 2.5+ โ โ (debt reduced) โ โ 1000.9 USDT โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Step 4: โ
โ Repay Flash Loan โ
โ โ
โ Approve: 1000.9 USDT โ
โ Aave auto-pulls at โ
โ function end โ
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Step 5: โ
โ Return Excess โ
โ โ
โ Excess collateral โ userโ
โ Excess USDT โ user โ
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Step 6: โ
โ Verify โ
โ โ
โ require(hfAfter > 1.05) โ
โ require(hfAfter >= 2.5) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โ
Position Rebalanced
Health Factor: 1.05 โ 2.5
User paid: $0 (gasless)
Time: ~60 seconds
Key Innovation: Repay debt FIRST (improves health) โ THEN withdraw collateral (safe)
Traditional approach (withdraw โ swap โ repay) would trigger liquidation
Standard EIP-712 (chain-locked):
keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("TapThatXRegistry")),
keccak256(bytes("1")),
block.chainid, // โ Locks to specific chain
address(this)
))TapThat X (cross-chain compatible):
keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,address verifyingContract)"),
keccak256(bytes("TapThatXRegistry")),
keccak256(bytes("1")),
address(this)
// No chainId โ Chip works on any chain
))Benefit: Register chip once on Sepolia โ use on Base, OP, Mainnet, Arbitrum without re-registration.
Security: Maintained via per-chain nonce tracking in TapThatXProtocol.
Problem: HaLo library uses JSON (no BigInt support), but Viem uses BigInt for uint256 precision.
Solution: Recursive BigInt โ string conversion before NFC signing.
Implementation (useHaloChip.ts):
const serializeBigInt = (obj: any): any => {
if (typeof obj === "bigint") return obj.toString();
if (obj === null || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(serializeBigInt);
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, serializeBigInt(v)])
);
};
// Usage in signTypedData
const typedDataPayload = {
domain,
types,
primaryType,
value: serializeBigInt(message), // Recursive conversion
};
const result = await execHaloCmdWeb({
name: "sign",
keyNo: 1,
typedData: typedDataPayload,
});Example:
// Input
{ owner: "0x123", value: 1000000000000000000n, timestamp: 1698765432n }
// After serialization
{ owner: "0x123", value: "1000000000000000000", timestamp: "1698765432" }Traditional Approach (fails):
1. Withdraw collateral from Aave
2. Swap collateral for debt token
3. Repay debt
โ Fails: Withdrawing before repaying triggers liquidation
TapThat X Approach (succeeds):
1. Flash loan debt token
2. Repay debt (health factor improves)
3. Withdraw collateral (now safe)
4. Swap collateral for debt token
5. Repay flash loan
โ
Succeeds: Debt repayment improves health before withdrawal
Key Insight: Improving health factor BEFORE withdrawing enables safe collateral removal.
Traditional Gasless (requires relayer trust):
User sends tokens to relayer โ Relayer submits transaction
โ Risk: Relayer could steal tokens
TapThat X Gasless (trustless):
// User approves once
IERC20(token).approve(extensionAddress, maxUint256);
// Extension pulls during execution (relayer can't access)
IERC20(token).transferFrom(owner, address(this), amount);
// Smart contract validates chip signature (relayer can't forge)
require(registry.hasChip(owner, chip), "Ownership validation");Security: Relay pays gas but cannot steal funds. On-chain validation prevents forgery.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend Structure โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app/
โโโ page.tsx โ Landing page (/)
โโโ register/page.tsx โ Chip registration (/register)
โโโ approve/page.tsx โ Token approval (/approve)
โโโ configure/page.tsx โ Action configuration (/configure)
โโโ execute/page.tsx โ Tap-to-execute (/execute)
hooks/
โโโ useHaloChip.ts โ NFC hardware integration
โโโ useGaslessRelay.ts โ Backend relay API
utils/
โโโ actionTemplates.ts โ CallData builders
โโโ erc20TransferTemplate
โโโ aaveRebalanceTemplate
โโโ bridgeETHTemplate
api/
โโโ relay-execute-tap/route.ts โ Gasless relay endpoint
Purpose: Interface to HaLo NFC chip hardware via Web NFC API
Functions:
const { signMessage, signTypedData, isLoading, error } = useHaloChip();
// Chip detection (returns address)
const { address, signature } = await signMessage({
message: "init",
format: "text"
});
// EIP-712 signing
const { address, signature } = await signTypedData({
domain: { name: "TapThatXProtocol", version: "1", verifyingContract },
types: { CallAuthorization: [...] },
primaryType: "CallAuthorization",
message: { owner, target, callData, value, timestamp, nonce }
});Library: @arx-research/libhalo@1.3.2
Platforms:
- โ Android Chrome (native Web NFC)
โ ๏ธ iOS Safari (requires NFC-enabled browser or app)
Purpose: Submit transactions to gasless relay API
Function:
const { relayExecuteTap } = useGaslessRelay();
const result = await relayExecuteTap({
owner: "0x...",
chip: "0x...",
chipSignature: "0x...",
timestamp: 1234567890,
nonce: "0x...",
});
// Returns: { success: true, transactionHash: "0x...", blockNumber: "12345" }Endpoint: POST /api/relay-execute-tap
Purpose: Build callData for common DeFi operations
ERC20 Transfer Template:
const erc20TransferTemplate = {
id: "erc20-transfer",
name: "ERC20 Transfer",
buildCallData: (params: {
tokenAddress: `0x${string}`;
from: `0x${string}`;
to: `0x${string}`;
amount: bigint;
}) => {
const callData = encodeFunctionData({
abi: ERC20_ABI,
functionName: "transferFrom",
args: [params.from, params.to, params.amount],
});
return { target: params.tokenAddress, callData, value: 0n };
},
};Aave Rebalance Template:
const aaveRebalanceTemplate = {
id: "aave-rebalance",
buildCallData: (params: {
rebalancerAddress: `0x${string}`;
owner: `0x${string}`;
collateralAsset: `0x${string}`;
debtAsset: `0x${string}`;
targetHealthFactor: bigint;
maxSlippage: bigint;
}) => {
const callData = encodeFunctionData({
abi: AAVE_REBALANCER_ABI,
functionName: "executeRebalance",
args: [
params.owner,
{
collateralAsset: params.collateralAsset,
debtAsset: params.debtAsset,
targetHealthFactor: params.targetHealthFactor,
maxSlippage: params.maxSlippage,
},
],
});
return { target: params.rebalancerAddress, callData, value: 0n };
},
};| Network | Chain ID | Purpose | Contracts |
|---|---|---|---|
| Ethereum Sepolia | 11155111 | Bridge testing | Core (5) + Bridge (1) |
| Base Sepolia | 84532 | Full features | Core (5) + DeFi Extensions (3) + MockUSDC |
| Contract | Address |
|---|---|
| MockUSDC | 0xb090ae6dd89b25d1b79718853c439d1354bf62c5 |
| TapThatXRegistry | 0x91D05d5B8913BCdA59f1923dC6831B108154Df22 |
| TapThatXProtocol | 0x0F917750db157D65c6c14e5Ce5828a250569afE1 |
| TapThatXConfiguration | See deployedContracts.ts |
| TapThatXExecutor | See deployedContracts.ts |
| TapThatXAaveRebalancer | See deployedContracts.ts |
| TapThatXAavePositionCloser | See deployedContracts.ts |
| Contract | Address |
|---|---|
| TapThatXRegistry | See deployedContracts.ts |
| TapThatXProtocol | See deployedContracts.ts |
| TapThatXConfiguration | See deployedContracts.ts |
| TapThatXExecutor | See deployedContracts.ts |
| TapThatXBridgeETHViaWETH | See deployedContracts.ts |
- โ ERC20 transfers
- โ Aave V3 flash loan rebalancing
- โ Aave V3 position closing
- โ MockUSDC for testing
- โ Sub-2-second block times
- โ <$0.01 gas costs
- โ Bridge to Base Sepolia
- โ Bridge to OP Sepolia
- โ WETH unwrapping
- โ Dual L2 deposits
| Operation | Gas | Time | Cost (1 gwei) | User Cost |
|---|---|---|---|---|
| Register Chip | 150k | ~30s | 0.00015 ETH | Paid once |
| Configure Action | 80k | ~30s | 0.00008 ETH | Paid once |
| Execute ERC20 Transfer | 130k | ~3s | 0.00013 ETH | $0 (relayed) |
| Aave Rebalance (Flash Loan) | 1.5M | ~60s | 0.0015 ETH | $0 (relayed) |
| Bridge to 2 L2s | 3M | ~2min | 0.003 ETH | $0 (relayed) |
Key Insight: Users pay setup costs once, then execute unlimited actions gaslessly.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Security Validation Layers โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Request from User
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Layer 1: Signature โ
โ - EIP-712 format โ
โ - Recover chip addr โ
โ - Verify signature โ
โโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
Valid signature
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Layer 2: Ownership โ
โ - registry.hasChip()โ
โ - owner โ chip map โ
โ - Bidirectional โ
โโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
Owner has chip
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Layer 3: Timestamp โ
โ - Not in future โ
โ - Within 5 min โ
โ - Prevents stale โ
โโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
Fresh authorization
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Layer 4: Nonce โ
โ - Check unused โ
โ - Mark used โ
โ - Replay protection โ
โโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
Unique transaction
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Layer 5: Execution โ
โ - ReentrancyGuard โ
โ - target.call() โ
โ - Return success โ
โโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โ
Transaction executes
| Feature | Implementation | Purpose |
| -------------------------- | ----------------------------------------------------------------------------- | ---------------------------- | --------------------- | ----------------------- |
| Replay Protection | mapping(bytes32 => bool) usedNonces | Prevent signature reuse |
| Timestamp Validation | require(timestamp <= block.timestamp && block.timestamp - timestamp <= 300) | Prevent stale signatures |
| Ownership Verification | require(registry.hasChip(owner, chip)) | Ensure chip belongs to owner |
| ReentrancyGuard | OpenZeppelin protection | Prevent reentrancy attacks |
| Access Control | require(msg.sender == PROTOCOL | | msg.sender == owner) | Limit extension callers |
| Secure Element | HaLo chip private key never leaves chip | Hardware security |
- Foundry - Development framework
- Solidity ^0.8.19
- OpenZeppelin - EIP712, ECDSA, ReentrancyGuard, Ownable
- Aave V3 - Flash loans (Base Sepolia:
0x8bAB6d1b75f19e9eD9fCe8b9BD338844fF79aE27) - Uniswap V2 - Token swaps
- Next.js 15 (App Router)
- React 19
- TypeScript 5
- Wagmi 2.16 - React hooks for Ethereum
- Viem 2.34 - TypeScript Ethereum library
- RainbowKit 2.2 - Wallet connection
- @arx-research/libhalo 1.3.2 - NFC chip communication
- TailwindCSS 4 - Utility-first CSS
- DaisyUI 5 - Component library
- Lucide React - Icon library
- Glassmorphism design aesthetic
- Base Sepolia - L2 deployment (2s blocks, <$0.01 gas)
- Optimism Sepolia - L2 deployment
- Ethereum Sepolia - L1 deployment (bridge base)
- Alchemy - RPC provider (optional)
- Arx HaLo NFC Chips - Secure element with EIP-712 signing
- Web NFC API - Browser-based NFC reading (Android Chrome)
- Node.js >= 22.14.0
- npm or yarn
- MetaMask or compatible Web3 wallet
- NFC-capable mobile device (Android recommended)
- Arx HaLo NFC chip (order here)
# Clone repository
git clone https://github.com/0xgeorgemathew/tap-that-x.git
cd tap-that-x
# Install dependencies
npm installCreate .env file in packages/nextjs/:
# Relayer account (must be funded with testnet ETH)
RELAYER_PRIVATE_KEY=0x...
# Alchemy API key (optional, improves RPC reliability)
ALCHEMY_API_KEY=...
# Or use public name
NEXT_PUBLIC_ALCHEMY_API_KEY=...Fund Relayer Account:
- Generate new wallet or use existing private key
- Get testnet ETH from Base Sepolia Faucet
- Add private key to
.env
# Start Next.js development server
npm run start
# Visit http://localhost:3000# Run Foundry tests
npm run foundry:test
# Run with verbosity
npm run foundry:test -- -vvv
# Test specific contract
npm run foundry:test -- --match-contract TapThatXTestTest Coverage:
- โ Chip registration
- โ ERC20 authorized calls
- โ Nonce replay protection
- โ Timestamp expiration
- โ Ownership validation
- โ Aave rebalancing
- โ Flash loan calculations
# Deploy to Base Sepolia
npm run deploy
# Deploy to Sepolia (bridge testing)
npm run deploy -- --network sepoliaDeployment Script: packages/foundry/script/Deploy.s.sol
Deployment Order:
- TapThatXRegistry (no dependencies)
- TapThatXProtocol (requires Registry)
- TapThatXConfiguration (requires Registry)
- TapThatXExecutor (requires Protocol + Configuration)
- Extensions (network-specific)
1. Navigate to /register
2. Click "Start Registration"
3. Tap chip to detect address (2-3 sec)
4. Tap again to authorize registration (2-3 sec)
5. Confirm transaction in wallet (5-10 sec)
โ
Chip registered on-chain
1. Navigate to /approve
2. Click "Approve USDC Spending"
3. Confirm in wallet
โ
Protocol can spend your USDC (you control via allowance)
1. Navigate to /configure
2. Select your registered chip
3. Choose action template:
- ERC20 Transfer
- Aave Rebalance
- Bridge ETH
4. Enter parameters (recipient, amount, etc.)
5. Save configuration on-chain
โ
Chip now executes this action on tap
1. Navigate to /execute
2. Tap chip to detect (2 sec)
3. Tap again to authorize (2 sec)
4. Transaction executes gaslessly (1 sec)
โ
Action completed, no wallet popup needed
# 1. Start local Foundry chain
npm run chain
# 2. Deploy contracts locally
npm run deploy -- --network localhost
# 3. Start frontend
npm run start
# 4. Test with mock chip address
# Use any address as "chip" for local testing# 1. Deploy to Base Sepolia
npm run deploy
# 2. Fund relayer account
# Get ETH from https://www.alchemy.com/faucets/base-sepolia
# 3. Get HaLo chip
# Order from https://arx.org/
# 4. Test full flow
# register โ approve โ configure โ execute- NFC chip registration with EIP-712
- Generic executeAuthorizedCall for any contract
- Gasless execution via relay
- ERC20 token transfers
- Aave V3 flash loan rebalancing
- Aave V3 position closing
- Cross-chain bridging (Sepolia โ Base/OP)
- Chain-agnostic chip signatures
- Dynamic gas limit detection
- Mobile-first UI with glassmorphism
- Mainnet deployment
- Additional DeFi integrations (Uniswap V3, Compound)
- Multi-action batching (execute multiple actions in one tap)
- Spending limits per chip
- Emergency pause functionality
- Avail Nexus Integration - Cross-chain payments with unified liquidity
- Merchant Terminals - Physical POS systems accepting stablecoin payments
- NFT Ticketing - Tap-to-claim POAPs and event tickets
- DAO Voting - Physical presence voting with chips
- DCA Strategies - Automated dollar-cost averaging via taps
- Yield Optimization - Auto-rebalance across protocols
- Limit Orders - DEX limit orders with tap triggers
Contributions welcome! Please read CONTRIBUTING.md first.
# 1. Fork repository
# 2. Create feature branch
git checkout -b feature/amazing-feature
# 3. Make changes
# 4. Run tests
npm run foundry:test
# 5. Commit changes
git commit -m "Add amazing feature"
# 6. Push to branch
git push origin feature/amazing-feature
# 7. Open Pull RequestThis project is licensed under the MIT License - see the LICENSE file for details.
- Arx Research for HaLo NFC chips and libhalo library
- Base for fast, cheap L2 infrastructure
- Optimism for L2StandardBridge specification
- Aave for V3 flash loan functionality
- Scaffold-ETH for development framework
- OpenZeppelin for battle-tested smart contract libraries
- Twitter: @0xgeorgemathew
- GitHub: @0xgeorgemathew
- Project: TapThat X
Built with โค๏ธ for ETHOnline 2025 Hackathon
Making blockchain physical, one tap at a time